Clarify model-generated and legacy app path types (#28577)

## Why

`ApiPathString` kind of implies that it can be used anywhere we pull a
path out of JSON, but it's not really appropriate for tool arguments
when the model might generate relative paths.

Prefer `String` for model-generated paths and we can handle the
conversion per feature for now and define a shared abstraction later if
it makes sense.

# What

Rename `ApiPathString` to `AppLegacyPathString` to clarify its role.

Expand the `path-types` skill to tell the model to leave tool args as
bare strings.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-16 13:47:43 -07:00
committed by GitHub
Unverified
parent a50671e748
commit 322b83de5e
22 changed files with 144 additions and 126 deletions
+3 -1
View File
@@ -9,10 +9,12 @@ Apply this guidance when defining new types. Change existing code only when expl
and keep edits minimal and proportional. Treat these rules as the target state of an ongoing
migration; if compliance is difficult, ask the user how to proceed.
- In app-server protocol types, use `ApiPathString` for backwards compatibility during the URI
- In app-server protocol types, use `LegacyAppPathString` for backwards compatibility during the URI
migration. At the protocol boundary, convert it to `PathUri` and use `PathUri` internally. For
host-local logic, such as some config values, use `AbsolutePathBuf` or `PathBuf` instead.
- In exec-server protocol types, use `PathUri`. Internally, use `PathUri` or `AbsolutePathBuf` as
appropriate.
- In dependencies shared by both servers, use `PathUri` or separate APIs that decouple their use
cases.
- Tool call arguments that the model is expected to generate should be deserialized as regular
`String`s with feature-specific path handling code.
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -84,9 +84,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"CommandAction": {
"oneOf": [
{
@@ -289,7 +286,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -472,6 +469,9 @@
}
]
},
"LegacyAppPathString": {
"type": "string"
},
"NetworkApprovalContext": {
"properties": {
"host": {
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -58,9 +58,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
@@ -74,7 +71,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -257,6 +254,9 @@
}
]
},
"LegacyAppPathString": {
"type": "string"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
@@ -23,7 +23,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -33,7 +33,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -54,9 +54,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
@@ -70,7 +67,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -278,6 +275,9 @@
},
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"PermissionGrantScope": {
"enum": [
"turn",
@@ -107,7 +107,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -117,7 +117,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -164,9 +164,6 @@
"AgentPath": {
"type": "string"
},
"ApiPathString": {
"type": "string"
},
"AppBranding": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
@@ -1345,7 +1342,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -2339,6 +2336,9 @@
],
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"McpServerOauthLoginCompletedNotification": {
"properties": {
"error": {
+6 -6
View File
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -84,9 +84,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"ApplyPatchApprovalParams": {
"properties": {
"callId": {
@@ -643,7 +640,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -826,6 +823,9 @@
}
]
},
"LegacyAppPathString": {
"type": "string"
},
"McpElicitationArrayType": {
"enum": [
"array"
@@ -6071,7 +6071,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/v2/ApiPathString"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"type": [
"array",
@@ -6081,7 +6081,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/v2/ApiPathString"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"type": [
"array",
@@ -6193,9 +6193,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"AppBranding": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
@@ -9505,7 +9502,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/v2/ApiPathString"
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"type": {
"enum": [
@@ -11184,6 +11181,9 @@
"title": "ItemStartedNotification",
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"ListMcpServerStatusParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -273,7 +273,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -283,7 +283,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -395,9 +395,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"AppBranding": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
@@ -5798,7 +5795,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -7637,6 +7634,9 @@
"title": "ItemStartedNotification",
"type": "object"
},
"LegacyAppPathString": {
"type": "string"
},
"ListMcpServerStatusParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -58,9 +58,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"AutoReviewDecisionSource": {
"description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.",
"enum": [
@@ -81,7 +78,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -536,6 +533,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
@@ -27,7 +27,7 @@
"read": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -37,7 +37,7 @@
"write": {
"description": "This will be removed in favor of `entries`.",
"items": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
@@ -58,9 +58,6 @@
},
"type": "object"
},
"ApiPathString": {
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
@@ -74,7 +71,7 @@
{
"properties": {
"path": {
"$ref": "#/definitions/ApiPathString"
"$ref": "#/definitions/LegacyAppPathString"
},
"type": {
"enum": [
@@ -529,6 +526,9 @@
],
"type": "string"
},
"LegacyAppPathString": {
"type": "string"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
@@ -23,4 +23,4 @@
* [`AbsolutePathBuf`]. Relative path text remains valid until an operation
* such as [`Self::to_path_uri`] requires an absolute path.
*/
export type ApiPathString = string;
export type LegacyAppPathString = string;
+1 -1
View File
@@ -4,7 +4,6 @@ export type { AbsolutePathBuf } from "./AbsolutePathBuf";
export type { AgentMessageInputContent } from "./AgentMessageInputContent";
export type { AgentPath } from "./AgentPath";
export type { AmazonBedrockCredentialSource } from "./AmazonBedrockCredentialSource";
export type { ApiPathString } from "./ApiPathString";
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
export type { AuthMode } from "./AuthMode";
@@ -43,6 +42,7 @@ export type { InitializeParams } from "./InitializeParams";
export type { InitializeResponse } from "./InitializeResponse";
export type { InputModality } from "./InputModality";
export type { InternalSessionSource } from "./InternalSessionSource";
export type { LegacyAppPathString } from "./LegacyAppPathString";
export type { LocalShellAction } from "./LocalShellAction";
export type { LocalShellExecAction } from "./LocalShellExecAction";
export type { LocalShellStatus } from "./LocalShellStatus";
@@ -1,15 +1,15 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ApiPathString } from "../ApiPathString";
import type { LegacyAppPathString } from "../LegacyAppPathString";
import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
export type AdditionalFileSystemPermissions = {
/**
* This will be removed in favor of `entries`.
*/
read: Array<ApiPathString> | null,
read: Array<LegacyAppPathString> | null,
/**
* This will be removed in favor of `entries`.
*/
write: Array<ApiPathString> | null, globScanMaxDepth?: number, entries?: Array<FileSystemSandboxEntry>, };
write: Array<LegacyAppPathString> | null, globScanMaxDepth?: number, entries?: Array<FileSystemSandboxEntry>, };
@@ -1,7 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ApiPathString } from "../ApiPathString";
import type { LegacyAppPathString } from "../LegacyAppPathString";
import type { FileSystemSpecialPath } from "./FileSystemSpecialPath";
export type FileSystemPath = { "type": "path", path: ApiPathString, } | { "type": "glob_pattern", pattern: string, } | { "type": "special", value: FileSystemSpecialPath, };
export type FileSystemPath = { "type": "path", path: LegacyAppPathString, } | { "type": "glob_pattern", pattern: string, } | { "type": "special", value: FileSystemSpecialPath, };
@@ -16,7 +16,7 @@ use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::ApiPathString;
use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathConvention;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -57,9 +57,9 @@ impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {
#[ts(export_to = "v2/")]
pub struct AdditionalFileSystemPermissions {
/// This will be removed in favor of `entries`.
pub read: Option<Vec<ApiPathString>>,
pub read: Option<Vec<LegacyAppPathString>>,
/// This will be removed in favor of `entries`.
pub write: Option<Vec<ApiPathString>>,
pub write: Option<Vec<LegacyAppPathString>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub glob_scan_max_depth: Option<NonZeroUsize>,
@@ -78,7 +78,7 @@ impl From<CoreFileSystemPermissions<AbsolutePathBuf>> for AdditionalFileSystemPe
if let Some(paths) = read.as_ref() {
entries.extend(paths.iter().map(|path| FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: ApiPathString::from_abs_path(path),
path: LegacyAppPathString::from_abs_path(path),
},
access: FileSystemAccessMode::Read,
}));
@@ -86,14 +86,24 @@ impl From<CoreFileSystemPermissions<AbsolutePathBuf>> for AdditionalFileSystemPe
if let Some(paths) = write.as_ref() {
entries.extend(paths.iter().map(|path| FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: ApiPathString::from_abs_path(path),
path: LegacyAppPathString::from_abs_path(path),
},
access: FileSystemAccessMode::Write,
}));
}
Self {
read: read.map(|paths| paths.iter().map(ApiPathString::from_abs_path).collect()),
write: write.map(|paths| paths.iter().map(ApiPathString::from_abs_path).collect()),
read: read.map(|paths| {
paths
.iter()
.map(LegacyAppPathString::from_abs_path)
.collect()
}),
write: write.map(|paths| {
paths
.iter()
.map(LegacyAppPathString::from_abs_path)
.collect()
}),
glob_scan_max_depth: None,
entries: Some(entries),
}
@@ -276,7 +286,7 @@ impl From<FileSystemSpecialPath> for CoreFileSystemSpecialPath {
#[ts(export_to = "v2/")]
// TODO(anp): Rename this type to distinguish it from the generic protocol FileSystemPath.
pub enum FileSystemPath {
Path { path: ApiPathString },
Path { path: LegacyAppPathString },
GlobPattern { pattern: String },
Special { value: FileSystemSpecialPath },
}
@@ -286,7 +296,7 @@ impl From<CoreFileSystemPath<AbsolutePathBuf>> for FileSystemPath {
fn from(value: CoreFileSystemPath<AbsolutePathBuf>) -> Self {
match value {
CoreFileSystemPath::Path { path } => Self::Path {
path: ApiPathString::from_abs_path(&path),
path: LegacyAppPathString::from_abs_path(&path),
},
CoreFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },
CoreFileSystemPath::Special { value } => Self::Special {
@@ -35,7 +35,7 @@ use codex_protocol::user_input::UserInput as CoreUserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use codex_utils_path_uri::ApiPathString;
use codex_utils_path_uri::LegacyAppPathString;
use pretty_assertions::assert_eq;
use serde_json::Value as JsonValue;
use serde_json::json;
@@ -579,8 +579,8 @@ fn additional_file_system_permissions_populates_entries_for_legacy_roots() {
);
let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone());
let read_only_api_path = ApiPathString::from_abs_path(&read_only_path);
let read_write_api_path = ApiPathString::from_abs_path(&read_write_path);
let read_only_api_path = LegacyAppPathString::from_abs_path(&read_only_path);
let read_write_api_path = LegacyAppPathString::from_abs_path(&read_write_path);
assert_eq!(
permissions,
@@ -1,7 +1,7 @@
use std::ffi::OsStr;
use anyhow::Result;
use codex_utils_path_uri::ApiPathString;
use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
@@ -28,7 +28,7 @@ impl TestEnvironment {
}
}
pub(crate) fn remote_cwd(&self, instance_id: &str) -> Result<Option<ApiPathString>> {
pub(crate) fn remote_cwd(&self, instance_id: &str) -> Result<Option<LegacyAppPathString>> {
let path_uri = match self {
Self::Local => return Ok(None),
Self::Docker { .. } => {
@@ -40,7 +40,7 @@ impl TestEnvironment {
PathUri::parse(&format!("file:///C:/codex-core-test-cwd-{instance_id}"))?
}
};
Ok(Some(ApiPathString::from_path_uri(
Ok(Some(LegacyAppPathString::from_path_uri(
&path_uri,
self.path_convention(),
)?))
+1 -1
View File
@@ -164,7 +164,7 @@ pub(super) use codex_terminal_detection::TerminalInfo;
pub(super) use codex_terminal_detection::TerminalName;
pub(super) use codex_utils_absolute_path::AbsolutePathBuf;
pub(super) use codex_utils_approval_presets::builtin_approval_presets;
pub(super) use codex_utils_path_uri::ApiPathString;
pub(super) use codex_utils_path_uri::LegacyAppPathString;
pub(super) use crossterm::event::KeyCode;
pub(super) use crossterm::event::KeyEvent;
pub(super) use crossterm::event::KeyModifiers;
@@ -89,8 +89,8 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
.expect("absolute read path");
let write_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/write")))
.expect("absolute write path");
let read_api_path = ApiPathString::from_abs_path(&read_path);
let write_api_path = ApiPathString::from_abs_path(&write_path);
let read_api_path = LegacyAppPathString::from_abs_path(&read_path);
let write_api_path = LegacyAppPathString::from_abs_path(&write_path);
let request = exec_approval_request_from_params(
AppServerCommandExecutionRequestApprovalParams {
thread_id: "thread-1".to_string(),
@@ -276,8 +276,8 @@ fn app_server_request_permissions_preserves_file_system_permissions() {
.expect("absolute read path");
let write_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/write")))
.expect("absolute write path");
let read_api_path = ApiPathString::from_abs_path(&read_path);
let write_api_path = ApiPathString::from_abs_path(&write_path);
let read_api_path = LegacyAppPathString::from_abs_path(&read_path);
let write_api_path = LegacyAppPathString::from_abs_path(&write_path);
let cwd =
AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp"))).expect("absolute cwd");
+20 -17
View File
@@ -30,9 +30,9 @@ use ts_rs::TS;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, TS)]
#[serde(transparent)]
#[ts(type = "string")]
pub struct ApiPathString(String);
pub struct LegacyAppPathString(String);
impl ApiPathString {
impl LegacyAppPathString {
/// Renders an absolute path using the current host's path convention.
pub fn from_abs_path(path: &AbsolutePathBuf) -> Self {
Self(path.to_string_lossy().into_owned())
@@ -48,7 +48,7 @@ impl ApiPathString {
pub fn from_path_uri(
path: &PathUri,
convention: PathConvention,
) -> Result<Self, ApiPathStringError> {
) -> Result<Self, LegacyAppPathStringError> {
if let Some(path_bytes) = path.opaque_fallback_bytes() {
return render_opaque_fallback(path, &path_bytes, convention).map(Self);
}
@@ -61,12 +61,15 @@ impl ApiPathString {
/// Parses this API string as an absolute path using the requested native
/// path convention and returns its canonical path URI.
pub fn to_path_uri(&self, convention: PathConvention) -> Result<PathUri, ApiPathStringError> {
pub fn to_path_uri(
&self,
convention: PathConvention,
) -> Result<PathUri, LegacyAppPathStringError> {
let path = match convention {
PathConvention::Posix => parse_posix_path(&self.0),
PathConvention::Windows => parse_windows_path(&self.0),
};
path.ok_or_else(|| ApiPathStringError::InvalidNativePath {
path.ok_or_else(|| LegacyAppPathStringError::InvalidNativePath {
path: self.0.clone(),
convention,
})
@@ -102,7 +105,7 @@ impl ApiPathString {
}
}
impl From<AbsolutePathBuf> for ApiPathString {
impl From<AbsolutePathBuf> for LegacyAppPathString {
fn from(path: AbsolutePathBuf) -> Self {
Self::from_abs_path(&path)
}
@@ -194,7 +197,7 @@ fn render_opaque_fallback(
path: &PathUri,
path_bytes: &[u8],
convention: PathConvention,
) -> Result<String, ApiPathStringError> {
) -> Result<String, LegacyAppPathStringError> {
let rendered = match convention {
PathConvention::Posix if path_bytes.starts_with(b"/") => {
Some(String::from_utf8_lossy(path_bytes).into_owned())
@@ -202,7 +205,7 @@ fn render_opaque_fallback(
PathConvention::Windows => render_windows_opaque_fallback(path_bytes),
PathConvention::Posix => None,
};
rendered.ok_or_else(|| ApiPathStringError::OpaqueFallback {
rendered.ok_or_else(|| LegacyAppPathStringError::OpaqueFallback {
path: path.to_string(),
})
}
@@ -238,13 +241,13 @@ fn is_windows_separator(character: u16) -> bool {
character == u16::from(b'\\') || character == u16::from(b'/')
}
impl fmt::Display for ApiPathString {
impl fmt::Display for LegacyAppPathString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for ApiPathString {
impl Serialize for LegacyAppPathString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -253,9 +256,9 @@ impl Serialize for ApiPathString {
}
}
impl JsonSchema for ApiPathString {
impl JsonSchema for LegacyAppPathString {
fn schema_name() -> String {
"ApiPathString".to_string()
"LegacyAppPathString".to_string()
}
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
@@ -263,7 +266,7 @@ impl JsonSchema for ApiPathString {
}
}
fn render_posix_path(path: &PathUri) -> Result<String, ApiPathStringError> {
fn render_posix_path(path: &PathUri) -> Result<String, LegacyAppPathStringError> {
let url = path.to_url();
// POSIX file paths do not have a UNC authority, so `file://server/share`
// cannot be represented as `/share` without losing the server identity.
@@ -281,7 +284,7 @@ fn render_posix_path(path: &PathUri) -> Result<String, ApiPathStringError> {
Ok(rendered)
}
fn render_windows_path(path: &PathUri) -> Result<String, ApiPathStringError> {
fn render_windows_path(path: &PathUri) -> Result<String, LegacyAppPathStringError> {
let url = path.to_url();
let mut segments = path_segments(&url);
let mut rendered = String::new();
@@ -342,15 +345,15 @@ fn decode_native_segment(segment: &str) -> String {
String::from_utf8_lossy(&bytes).into_owned()
}
fn incompatible_convention(path: &PathUri, convention: PathConvention) -> ApiPathStringError {
ApiPathStringError::IncompatibleConvention {
fn incompatible_convention(path: &PathUri, convention: PathConvention) -> LegacyAppPathStringError {
LegacyAppPathStringError::IncompatibleConvention {
path: path.to_string(),
convention,
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ApiPathStringError {
pub enum LegacyAppPathStringError {
#[error("opaque fallback path URI `{path}` cannot be recovered as a native path")]
OpaqueFallback { path: String },
#[error("path URI `{path}` cannot be rendered using {convention} path syntax")]
@@ -301,21 +301,23 @@ fn renders_native_paths_from_shared_cases() {
for case in RENDER_CASES {
let path = PathUri::parse(case.uri).expect("valid file URI");
let expected = match case.expected {
RenderExpectation::RoundTrip(rendered) => Ok(ApiPathString(rendered.to_string())),
RenderExpectation::RenderOnly(rendered) => Ok(ApiPathString(rendered.to_string())),
RenderExpectation::RoundTrip(rendered) => Ok(LegacyAppPathString(rendered.to_string())),
RenderExpectation::RenderOnly(rendered) => {
Ok(LegacyAppPathString(rendered.to_string()))
}
RenderExpectation::Error(ExpectedError::OpaqueFallback) => {
Err(ApiPathStringError::OpaqueFallback {
Err(LegacyAppPathStringError::OpaqueFallback {
path: path.to_string(),
})
}
RenderExpectation::Error(ExpectedError::IncompatibleConvention) => {
Err(ApiPathStringError::IncompatibleConvention {
Err(LegacyAppPathStringError::IncompatibleConvention {
path: path.to_string(),
convention: case.convention,
})
}
};
let actual = ApiPathString::from_path_uri(&path, case.convention);
let actual = LegacyAppPathString::from_path_uri(&path, case.convention);
assert_eq!(actual, expected, "rendering {case:?}");
if let Ok(rendered) = &actual {
@@ -327,14 +329,15 @@ fn renders_native_paths_from_shared_cases() {
}
if let RenderExpectation::RoundTrip(rendered) = case.expected {
let api_path = serde_json::from_value::<ApiPathString>(serde_json::json!(rendered))
.expect("native path should deserialize from API text");
let api_path =
serde_json::from_value::<LegacyAppPathString>(serde_json::json!(rendered))
.expect("native path should deserialize from API text");
let reparsed = api_path
.to_path_uri(case.convention)
.expect("native path should parse using its convention");
assert_eq!(reparsed, path, "parsing {case:?}");
assert_eq!(
ApiPathString::from_path_uri(&reparsed, case.convention),
LegacyAppPathString::from_path_uri(&reparsed, case.convention),
Ok(api_path),
"round-tripping {case:?}"
);
@@ -345,7 +348,7 @@ fn renders_native_paths_from_shared_cases() {
#[test]
fn relative_api_path_serializes_and_deserializes_unchanged() {
for raw_path in [".", "subdir", "subdir/file.rs"] {
let path = serde_json::from_value::<ApiPathString>(serde_json::json!(raw_path))
let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path))
.expect("relative API path should deserialize");
assert_eq!(
@@ -358,13 +361,13 @@ fn relative_api_path_serializes_and_deserializes_unchanged() {
#[test]
fn relative_api_path_is_invalid_when_converted_to_a_path_uri() {
let raw_path = "subdir";
let path = serde_json::from_value::<ApiPathString>(serde_json::json!(raw_path))
let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path))
.expect("relative API path should deserialize");
assert_eq!(path.infer_absolute_path_convention(), None);
assert_eq!(
path.to_path_uri(PathConvention::Posix),
Err(ApiPathStringError::InvalidNativePath {
Err(LegacyAppPathStringError::InvalidNativePath {
path: raw_path.to_string(),
convention: PathConvention::Posix,
})
@@ -377,13 +380,13 @@ fn other_non_absolute_api_paths_cannot_be_converted_to_path_uris() {
(r"workspace\file.rs", PathConvention::Windows),
(r"C:file.rs", PathConvention::Windows),
] {
let path = serde_json::from_value::<ApiPathString>(serde_json::json!(raw_path))
let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path))
.expect("API path should deserialize without validation");
assert_eq!(path.infer_absolute_path_convention(), None);
assert_eq!(
path.to_path_uri(convention),
Err(ApiPathStringError::InvalidNativePath {
Err(LegacyAppPathStringError::InvalidNativePath {
path: raw_path.to_string(),
convention,
})
@@ -409,7 +412,7 @@ fn infers_absolute_path_conventions_from_api_text() {
(r"C:file.rs", None),
(r"\rooted-without-drive", None),
] {
let path = serde_json::from_value::<ApiPathString>(serde_json::json!(raw_path))
let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path))
.expect("API path should deserialize without validation");
assert_eq!(
@@ -426,7 +429,7 @@ fn foreign_absolute_syntax_deserializes_without_host_interpretation() {
(r"C:\workspace\file.rs", PathConvention::Windows),
("/workspace/file.rs", PathConvention::Posix),
] {
let path = serde_json::from_value::<ApiPathString>(serde_json::json!(raw_path))
let path = serde_json::from_value::<LegacyAppPathString>(serde_json::json!(raw_path))
.expect("foreign API path should deserialize");
assert_eq!(path.as_str(), raw_path);
@@ -444,8 +447,8 @@ fn renders_an_absolute_path_using_the_host_convention() {
.expect("native path should be absolute");
assert_eq!(
ApiPathString::from(path),
ApiPathString(native_path.to_string())
LegacyAppPathString::from(path),
LegacyAppPathString(native_path.to_string())
);
}
@@ -464,19 +467,19 @@ fn renders_native_non_unicode_windows_fallback_lossily() {
AbsolutePathBuf::from_absolute_path_checked(native_path).expect("absolute native path");
assert_eq!(
ApiPathString::from_abs_path(&native_path),
ApiPathString(r"C:\bad\".to_string())
LegacyAppPathString::from_abs_path(&native_path),
LegacyAppPathString(r"C:\bad\".to_string())
);
let path = PathUri::from_abs_path(&native_path);
assert_eq!(
ApiPathString::from_path_uri(&path, PathConvention::Windows),
Ok(ApiPathString(r"C:\bad\".to_string()))
LegacyAppPathString::from_path_uri(&path, PathConvention::Windows),
Ok(LegacyAppPathString(r"C:\bad\".to_string()))
);
assert_eq!(
ApiPathString::from_path_uri(&path, PathConvention::Posix),
Err(ApiPathStringError::OpaqueFallback {
LegacyAppPathString::from_path_uri(&path, PathConvention::Posix),
Err(LegacyAppPathStringError::OpaqueFallback {
path: path.to_string(),
})
);
@@ -485,13 +488,13 @@ fn renders_native_non_unicode_windows_fallback_lossily() {
#[test]
fn serializes_and_deserializes_as_a_string() {
let path = PathUri::parse("file:///workspace/src/lib.rs").expect("valid file URI");
let rendered = ApiPathString::from_path_uri(&path, PathConvention::Posix)
let rendered = LegacyAppPathString::from_path_uri(&path, PathConvention::Posix)
.expect("POSIX URI should render");
let json = serde_json::to_string(&rendered).expect("rendered path should serialize");
assert_eq!(json, r#""/workspace/src/lib.rs""#);
assert_eq!(
serde_json::from_str::<ApiPathString>(&json)
serde_json::from_str::<LegacyAppPathString>(&json)
.expect("rendered path should deserialize from a string"),
rendered
);
+2 -2
View File
@@ -19,8 +19,8 @@ use url::Url;
mod api_path_string;
pub use api_path_string::ApiPathString;
pub use api_path_string::ApiPathStringError;
pub use api_path_string::LegacyAppPathString;
pub use api_path_string::LegacyAppPathStringError;
pub use api_path_string::PathConvention;
pub const FILE_SCHEME: &str = "file";