diff --git a/AGENTS.md b/AGENTS.md index c43a3f71d..39e758679 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,16 @@ API documentation site for Forge OpenAPI specs, built on Astro. - **`packages/fern-forge`**: `astro-fern` extension that validates and adapts Forge OpenAPI metadata. - **`packages/docs-site`**: API documentation website consuming `astro-fern` and `fern-forge`. The OpenAPI spec is fetched from the `openapi.forge.json` GitHub release asset (or a local file via `FORGE_OPENAPI_SPEC`). - **`packages/cloudflare-terraform-docs`**: Generator for the site's Terraform documentation data, derived from the released Cloudflare Terraform provider. Temporary: removed once Forge supports Terraform natively (see the site's README). +- **`src/`**: The Rust engine: OpenAPI to linked model to plugin host. It reads only native `x-forge-*` extensions (`src/metadata/`); other tools' extensions are translated in `src/compat/`. + +## Vendor compatibility + +Only `src/compat/` knows vendor extensions (`x-fern-*`, `x-stainless-*`). It is sectioned off so it can be deleted as a whole. See [`src/compat/README.md`](src/compat/README.md). + +- Never name a vendor extension outside `src/compat/` and `tests/compat/`, comments and docs included. `tests/boundary.rs` enforces this (`just boundary`). +- Compat is behind the default-on `compat` feature and is only a fallback: native `x-forge-*` keys always win. +- Support for a new vendor key means mapping it onto an existing native primitive in an adapter. Add the native key first if there is none. +- The docs packages (`astro-fern`, `fern-forge`, `cloudflare-api-site`) read vendor keys themselves and are not covered by the boundary. ## Conventions diff --git a/Cargo.toml b/Cargo.toml index 184531c7a..46373a6f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,11 @@ name = "parse" harness = false [features] -default = ["cli"] +default = ["cli", "compat"] +# The compatibility layer: it reads the extensions other tools' metadata is +# written with in place of the native `x-forge-*` ones, when a document has +# no native one. Without it, only the native extensions mean anything. +compat = [] # The `forge` command. Library users can turn it off with # `default-features = false`. cli = ["dep:clap", "dep:serde_json"] @@ -49,6 +53,10 @@ required-features = ["cli"] name = "wasm" required-features = ["cli", "wasm"] +[[test]] +name = "compat" +required-features = ["compat"] + [lints.clippy] # https://rust-lang.github.io/rust-clippy/rust-1.88.0/index.html diff --git a/justfile b/justfile index 77cc872cf..c3b7d5819 100644 --- a/justfile +++ b/justfile @@ -27,6 +27,10 @@ lint: cargo clippy --locked --all-targets --all-features -- -D warnings cargo clippy --locked --all-targets --no-default-features -- -D warnings +# Check that no other tool's extension is named outside the compatibility layer +boundary: + cargo test --locked --test boundary + # Release builds # # Every way of getting release binaries puts them in target/binaries// diff --git a/packages/forge/plugin/forge.wit b/packages/forge/plugin/forge.wit index af93aa10e..7a489f74e 100644 --- a/packages/forge/plugin/forge.wit +++ b/packages/forge/plugin/forge.wit @@ -82,7 +82,7 @@ interface forge { json: option, } - /// The stage of the API a method is in, from `x-fern-availability`. + /// The stage of the API a method is in, from `x-forge-availability`. enum availability { alpha, beta, preview, generally-available, deprecated, } diff --git a/packages/forge/src/index.ts b/packages/forge/src/index.ts index 9b374eab9..d40d60c63 100644 --- a/packages/forge/src/index.ts +++ b/packages/forge/src/index.ts @@ -144,7 +144,7 @@ export interface Method { * way around. */ readonly deprecated: boolean; - /** The stage of the API the method is in, from `x-fern-availability`, or undefined if it doesn't say. */ + /** The stage of the API the method is in, from `x-forge-availability`, or undefined if it doesn't say. */ readonly availability: Availability | undefined; /** * Whether to leave the method out of listings of methods, such as a CLI's @@ -197,7 +197,7 @@ export interface Method { */ export type Location = { readonly in: 'path' } | { readonly in: 'query'; readonly exploded: boolean }; -/** The stage of the API a method is in, as `x-fern-availability` writes it. */ +/** The stage of the API a method is in, as `x-forge-availability` writes it. */ export type Availability = 'alpha' | 'beta' | 'preview' | 'generally-available' | 'deprecated'; /** @@ -267,7 +267,7 @@ export interface Field { * though the same model's field appears in responses: `readOnly`. */ readonly readOnly: boolean; - /** Whether the value is a secret, such as a password or a token, to keep out of logs: `x-sensitive`. */ + /** Whether the value is a secret, such as a password or a token, to keep out of logs: `x-forge-sensitive`. */ readonly sensitive: boolean; /** The `default` of the property's schema, or undefined if it has none. */ readonly default: Json | undefined; diff --git a/rfcs/0001-metadata-resolution-layer.md b/rfcs/0001-metadata-resolution-layer.md new file mode 100644 index 000000000..d68ac7806 --- /dev/null +++ b/rfcs/0001-metadata-resolution-layer.md @@ -0,0 +1,103 @@ +# RFC 0001: The metadata resolution layer + +## Summary + +The engine reads vendor metadata extensions (`x-fern-*`, `x-stainless-skip`) directly, so everything downstream of the linked model inherits the vendors' vocabulary and formats. This RFC gives the engine a native `x-forge-*` vocabulary and moves all vendor knowledge into one quarantined, feature-gated **compatibility layer** (`src/compat/`) that maps vendor keys onto native primitives. The layer is designed to be deleted as a whole. + +Output for existing specs is unchanged, and the release is non-breaking. + +## Motivation + +Vendor names are hardcoded in the serde key enums in `src/link/openapi.rs`, in value formats (Fern's availability words, Stainless's skip list), in validation messages, in the list of keys kept from plugins, and in the public docs, the WIT and the JS runtime. Anything downstream, including the cf codebase, depends on a vendor's extension format, and adding another vendor means touching all of it again. + +The released aggregate uses the keys forge reads about 11,400 times (`x-fern-sdk-*` 3,638 each, `x-fern-availability` 3,444, `x-fern-ignore` 449, `x-stainless-skip` 114, and about 150 name keys). It also carries keys forge does not read, such as `x-fern-enum` (3,652) and `x-stainless-terraform-configurability` (407). + +## Scope + +Goals: + +- Nothing in the engine (`src/`, `tests/`, `packages/forge`) outside `src/compat/` and `tests/compat/` names a vendor extension, enforced by a test. +- Output for existing specs is byte-identical; no meaningful parse-time regression. +- Native keys are applied first; the compatibility layer is only a fallback, and can be removed without changing what any `x-forge-*` key means. + +Non-goals: Fern's structural behavior (allOf/anyOf, inline request types), operation ID prefixing, the cf codebase's own Fern-to-Forge rename, dotted `operationId` derivation, and the docs packages in this repository, which read vendor keys themselves. Vendor keys with no Forge primitive (`x-fern-enum`, `x-fern-audiences`, `x-stability` and others) stay unread. + +## The native vocabulary + +| Native key | Value | Meaning | Compat source | +|---|---|---|---| +| `x-forge-method-name` | string | the method's name | `x-fern-sdk-method-name` | +| `x-forge-group-name` | dotted string, or array of strings, top level first | the group path; an empty path makes no method, as today | `x-fern-sdk-group-name`, read the same way | +| `x-forge-skip` | bool | operation or alias: no method; property schema: no field | `x-fern-ignore` (operation), `x-stainless-skip` (property; a list of targets counts as `false`) | +| `x-forge-availability` | `alpha`, `beta`, `preview`, `generally-available`, `deprecated` | lifecycle stage | `x-fern-availability` | +| `x-forge-model-name` | string | the model's name | `x-fern-type-name` | +| `x-forge-property-name` | string | the property's field name | `x-fern-property-name` | +| `x-forge-parameter-name` | string | the parameter's field name | `x-fern-parameter-name` | +| `x-forge-sensitive` | bool | the value is a secret, to mask | none; replaces `x-sensitive` | +| `x-sensitive` | bool | deprecated name of `x-forge-sensitive`, read only where that is not written | none | + +`x-forge-hidden`, `x-forge-require-confirmation`, `x-forge-aliases` and `x-forge-commands` are unchanged. + +Design notes: + +- **Explicit keys, not one positional name key.** Tools that inline `$ref` targets would make a positional `x-forge-name` silently rename fields. A misplaced explicit key can be ignored or flagged instead. +- **`model-name`** sets `Model::name`, the word the Rust API, the WIT and the JS runtime use. `type-name` would promise more than it does, `schema-name` clashes with `Model::schema_name`, and `class-name` and `title` are a language's word and OpenAPI's display string. +- **`skip`**, not `ignore`: it says what happens, and stands apart from `x-forge-hidden` (generated, but not listed). One word covers operations and properties. +- **Groups as a dotted string or an array.** Cloudflare's specs write dotted strings, so moving to the native key is a rename with the value unchanged. Fern reads a string as one group and never splits on dots, so for a string with a dot the adapter and Fern disagree. That is deliberate. FORGE-3 expects to rename the key to `x-forge-namespace`. +- **Every native key begins with `x-forge-`.** `x-sensitive` predates that rule: it stays readable (155 production uses) as a deprecated alias, which is part of the native vocabulary and keeps working with compat off. +- **No misplacement validation.** A native key at the wrong level is silently ignored, as Fern's keys are today. Validating later only tightens. + +## Resolution rules + +1. Keys rank in three tiers: native, then deprecated native alias, then compat. The strongest tier present wins, per primitive and whatever order the keys are written in. Only the winning value is validated. +2. The same key written twice is an error. A value of the wrong JSON type is an error whichever key it is on. +3. Names with no letters or digits behave as they always have: a property, parameter or model name is ignored, a method name or group path that is empty makes no method, and a non-empty one that has none is an error. +4. Errors name the key the author actually wrote. +5. Writing native and compat keys with the same value is the supported way to stay portable across engine versions during the transition. + +A consequence of rule 1: a stale compat key silently diverges from a native key written beside it. + +## Architecture + +``` +src/ +├─ metadata/ the native table, per kind of object (definition, parameter, +│ schema, document): keys, values, formats. Pure: `&str` in, +│ small values out. Imports neither compat nor link. +├─ compat/ the adapters (fern, stainless) and their aggregator. The only +│ code holding vendor keys. Behind the default-on `compat` feature. +└─ link/ + ├─ resolve.rs native first, then compat; the only place link calls compat + └─ openapi.rs OpenAPI's own keys; every `x-` key asks resolve.rs +``` + +- Dependencies: `link` uses `metadata` and, behind the feature, `compat`; `compat` uses `metadata`. +- The layer maps keys to primitives and normalizes simple values. Structural behavior, such as aliases becoming methods, stays in the builder. +- Without the `compat` feature, vendor keys are ordinary extensions and are passed to plugins. +- Deleting the layer means removing `src/compat/`, `tests/compat/`, `mod compat` in `src/lib.rs`, the compat arms in `link/resolve.rs`, and the feature in `Cargo.toml`. No native key changes. A little native-side code that only the layer used is left over and can be removed afterwards. + +## Enforcement + +- Every vendor key literal is private to its adapter, and the compiler enforces that. +- `tests/boundary.rs` searches `src/`, `tests/` and `packages/forge/` (docs and WIT included) for `x-fern` and `x-stainless` outside the layer. +- CI compiles the crate without `compat` through clippy, but runs tests only with all features, to avoid a second full compile for a configuration nothing ships. + +## Verification + +Checked against the unmodified engine on the released aggregate (27.6 MB): + +- The original 379 Fern-keyed parse tests passed before any fixture was migrated. +- `forge --target ts` writes the same 3,381 files byte for byte, and the full model dump (197,702 lines) is identical, natively and through the WASM plugin boundary. +- `cargo test` passes with compat on, and with it off when run locally; clippy is clean in the library-only, compat-off and all-features configurations. +- Parse time (divan `parse` bench, paired base-new-new-base runs) is 0.8% slower (+0.34 ms of 42.0 ms, t = 1.9). Peak memory is +0.07%, with the same allocation count. + +## Alternatives considered + +- **Resolve after parse** (keep every `x-` key as raw JSON, normalize in a second pass): it must retain every extension on the 27 MB aggregate unless the reader consults a key table anyway, which collapses into this design. Rejected on memory and diff size. +- **A `Vendor` trait** with an implementation per tool: no primitive overlaps across vendors today, so the table just grows rows. Over-engineering for now. +- **Separate words for operations and properties** (`ignore` and `skip`): two words for one concept. +- **Arrays only for `x-forge-group-name`**: every migration would reshape values as well as rename keys. +- **Reading a string group as one group, as Fern does**: the native and Fern keys would read the same value differently. +- **Erroring when native and compat keys disagree**: stricter, but costs comparison machinery for no gain on correct specs. Silent native-wins was chosen. +- **A separate `metadata` crate now**: it would pull `Availability` and the other primitive types out of the engine. Deferred until a standalone consumer exists; the pure contract keeps that move mechanical. +- **Renaming `Group` and `Availability` now**: both break the plugin contract, so both wait for FORGE-3's single breaking release. diff --git a/src/compat/README.md b/src/compat/README.md new file mode 100644 index 000000000..6ebaee086 --- /dev/null +++ b/src/compat/README.md @@ -0,0 +1,47 @@ +# The compatibility layer + +The engine understands one vocabulary: the native `x-forge-*` extensions in +[`src/metadata/`](../metadata/mod.rs). This folder is the only place that +knows any other tool's extensions (Fern's `x-fern-*`, Stainless's +`x-stainless-*`). Each adapter maps its tool's keys onto native primitives. + +The layer is meant to be deleted as a whole once nobody writes those keys. +Everything below follows from that. + +## Rules + +- **Only this folder names a vendor extension.** That includes the readers, + the linker, the plugin host, the WIT, the `packages/forge` npm package, and + their comments and docs. `tests/boundary.rs` fails, with file and line, if + one appears elsewhere (`just boundary`; it also runs with `cargo test`). + Vendor tests live in `tests/compat/`. +- **Native wins.** A vendor key is read only for a primitive the document + has no native key for. When both are written, the native value is used and + the vendor value is not even validated, whatever order they appear in. +- **Behind the `compat` feature** (on by default). Native keys must mean the + same with it off. CI compiles that configuration (clippy with + `--no-default-features`) but does not run its tests. +- **Mapping, not semantics.** An adapter says which native primitive a key + stands for and how its value is written, as data. Anything structural, such + as how aliases become methods, belongs to the builder, not here. +- **No vendor knowledge leaks out.** Keys are private to their adapter. + `mod.rs` is the only way in: one resolver per kind of object, and one name + lookup per kind so errors can name the key the author wrote. + +## Adding a vendor key + +1. Find the native primitive it stands for in `src/metadata/`. If there is + none, add a native `x-forge-*` key first: a vendor key never introduces a + primitive. +2. Map it in the adapter for its tool, or add an adapter next to `fern.rs` + and `stainless.rs` and register it in `mod.rs`. +3. Add a test in `tests/compat/` showing that the vendor-keyed document + parses to the same `Forge` as the native-keyed one. + +## Removing the layer + +Delete this folder and `tests/compat/`, `mod compat` in `src/lib.rs`, the +`compat` arms in `src/link/resolve.rs`, and the `compat` feature and its +`[[test]]` entry in `Cargo.toml`. No native extension changes. A few +native-side items exist only for the layer and are left unused; removing them +is a mechanical follow-up. diff --git a/src/compat/fern.rs b/src/compat/fern.rs new file mode 100644 index 000000000..771c76f45 --- /dev/null +++ b/src/compat/fern.rs @@ -0,0 +1,157 @@ +//! The adapter for Fern's `x-fern-*` extensions. +//! +//! A group path written as one string is split on dots, as for +//! `x-forge-group-name`, which Fern itself does not do: Fern reads a string as a +//! single group. A document written for Fern uses the array, which means the +//! same in both. +//! +//! Its other extensions, such as `x-fern-enum` and `x-fern-audiences`, are +//! not read: they say things forge has no primitive for. + +/// What every key of the adapter begins with. A key without it is turned +/// away before it is matched against the table. +const PREFIX: &str = "x-fern-"; + +/// The keys of an operation or an alias. +pub(super) mod definition { + use super::PREFIX; + use crate::metadata::{BoolFormat, DefinitionField, Hit}; + + const METHOD_NAME: &str = "x-fern-sdk-method-name"; + const GROUP_NAME: &str = "x-fern-sdk-group-name"; + const IGNORE: &str = "x-fern-ignore"; + const AVAILABILITY: &str = "x-fern-availability"; + + /// The names of the adapter's keys of an operation or an alias, by slot. + pub(crate) const NAMES: [&str; 4] = [METHOD_NAME, GROUP_NAME, IGNORE, AVAILABILITY]; + + pub(crate) fn read(key: &str) -> Option> { + if !key.starts_with(PREFIX) { + return None; + } + Some(match key { + METHOD_NAME => Hit::compat(DefinitionField::Method, 0), + GROUP_NAME => Hit::compat(DefinitionField::Group, 1), + IGNORE => Hit::compat(DefinitionField::Skip(BoolFormat::Bool), 2), + AVAILABILITY => Hit::compat(DefinitionField::Availability, 3), + _ => return None, + }) + } +} + +/// The keys of a Parameter Object. +pub(super) mod parameter { + use super::PREFIX; + use crate::metadata::{Hit, ParameterField}; + + const NAME: &str = "x-fern-parameter-name"; + + /// The names of the adapter's keys of a Parameter Object, by slot. + pub(crate) const NAMES: [&str; 1] = [NAME]; + + pub(crate) fn read(key: &str) -> Option> { + if !key.starts_with(PREFIX) { + return None; + } + Some(match key { + NAME => Hit::compat(ParameterField::Name, 0), + _ => return None, + }) + } +} + +/// The keys of a Schema Object. +pub(super) mod schema { + use super::PREFIX; + use crate::metadata::{Hit, SchemaField}; + + const TYPE_NAME: &str = "x-fern-type-name"; + const PROPERTY_NAME: &str = "x-fern-property-name"; + + /// The names of the adapter's keys of a Schema Object, by slot. + pub(crate) const NAMES: [&str; 2] = [TYPE_NAME, PROPERTY_NAME]; + + pub(crate) fn read(key: &str) -> Option> { + if !key.starts_with(PREFIX) { + return None; + } + Some(match key { + TYPE_NAME => Hit::compat(SchemaField::ModelName, 0), + PROPERTY_NAME => Hit::compat(SchemaField::PropertyName, 1), + _ => return None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The names of the keys of a level, and whether the level reads a key. + type Level<'a> = (&'a [&'a str], fn(&str) -> bool); + use crate::metadata::{DefinitionField, Tier}; + + #[test] + fn each_key_is_read_as_itself_and_as_not_native() { + for key in definition::NAMES { + let hit = definition::read(key).unwrap_or_else(|| panic!("{key} is not a definition key")); + assert_eq!(hit.tier, Tier::Compat, "{key}"); + } + for key in parameter::NAMES { + let hit = parameter::read(key).unwrap_or_else(|| panic!("{key} is not a parameter key")); + assert_eq!(hit.tier, Tier::Compat, "{key}"); + } + for key in schema::NAMES { + let hit = schema::read(key).unwrap_or_else(|| panic!("{key} is not a schema key")); + assert_eq!(hit.tier, Tier::Compat, "{key}"); + } + } + + #[test] + fn a_key_is_read_only_at_its_level() { + let levels: [Level; 3] = [ + (&definition::NAMES, |key| definition::read(key).is_some()), + (¶meter::NAMES, |key| parameter::read(key).is_some()), + (&schema::NAMES, |key| schema::read(key).is_some()), + ]; + for (own, _) in levels { + for &key in own { + for (names, reads) in levels { + assert_eq!(reads(key), names.contains(&key), "{key}"); + } + } + } + } + + #[test] + fn slots_are_unique_among_the_keys_of_an_object() { + let mut slots: Vec = definition::NAMES + .iter() + .map(|key| definition::read(key).unwrap().slot) + .collect(); + slots.sort_unstable(); + slots.dedup(); + assert_eq!(slots.len(), definition::NAMES.len()); + assert!(slots.iter().all(|&slot| slot >= 32), "slots of other keys start at 32"); + } + + #[test] + fn the_group_is_read_as_the_native_group() { + let hit = definition::read("x-fern-sdk-group-name").unwrap(); + assert!(matches!(hit.field, DefinitionField::Group)); + } + + #[test] + fn unread_extensions_are_not_read() { + for key in [ + "x-fern-enum", + "x-fern-audiences", + "x-fern-pagination", + "x-fern-sdk-variables", + ] { + assert!(definition::read(key).is_none(), "{key}"); + assert!(parameter::read(key).is_none(), "{key}"); + assert!(schema::read(key).is_none(), "{key}"); + } + } +} diff --git a/src/compat/mod.rs b/src/compat/mod.rs new file mode 100644 index 000000000..46c87e2f8 --- /dev/null +++ b/src/compat/mod.rs @@ -0,0 +1,149 @@ +//! The compatibility layer: the keys other tools' metadata is written with, +//! read as the native extensions of [`metadata`](crate::metadata) say the +//! same things. +//! +//! Each adapter, [`fern`] and [`stainless`], maps its tool's keys onto the +//! native primitives and says, as data, what format each value is written +//! in. This module is what `link` asks, once per kind of object, after the +//! native keys have said no. It is the only way in: every key an adapter +//! reads is private to it. +//! +//! The layer is built only with the `compat` feature, and is meant to go: +//! once nobody writes these keys, deleting this folder and the calls to it in +//! `link/resolve.rs` changes nothing about what any native extension means. The keys are read in place of a native key only when there is none, +//! as `link` decides. + +mod fern; +mod stainless; + +use crate::metadata::{DefinitionField, Hit, ParameterField, SchemaField}; + +/// Returns what `key` says on an operation or an alias, if an adapter reads +/// it there. +pub(crate) fn definition(key: &str) -> Option> { + fern::definition::read(key) +} + +/// Returns what `key` says on a Parameter Object, if an adapter reads it +/// there. +pub(crate) fn parameter(key: &str) -> Option> { + fern::parameter::read(key) +} + +/// Returns what `key` says on a Schema Object, if an adapter reads it there. +/// Each adapter counts its keys from zero, so Stainless's come after Fern's. +pub(crate) fn schema(key: &str) -> Option> { + fern::schema::read(key).or_else(|| stainless::schema::read(key).map(|hit| hit.after(FERN_SCHEMA_KEYS))) +} + +/// How many keys of a Schema Object Fern has, which is where Stainless's +/// start. +const FERN_SCHEMA_KEYS: u8 = fern::schema::NAMES.len() as u8; + +/// Returns the name of the key of an operation or an alias that is `n`th +/// among the keys of the adapters. +pub(crate) fn definition_name(n: u8) -> &'static str { + fern::definition::NAMES[usize::from(n)] +} + +/// Returns the name of the key of a Parameter Object that is `n`th among the +/// keys of the adapters. +pub(crate) fn parameter_name(n: u8) -> &'static str { + fern::parameter::NAMES[usize::from(n)] +} + +/// Returns the name of the key of a Schema Object that is `n`th among the +/// keys of the adapters. +pub(crate) fn schema_name(n: u8) -> &'static str { + match n.checked_sub(FERN_SCHEMA_KEYS) { + None => fern::schema::NAMES[usize::from(n)], + Some(n) => stainless::schema::NAMES[usize::from(n)], + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The names of the keys of a level, and whether the level reads a key. + type Level<'a> = (&'a [&'a str], fn(&str) -> bool); + use crate::metadata::{self, COMPAT_SLOTS, Tier}; + + /// Asserts that each key in `names` is read by `read` as a key that is + /// not native, whose slot `name` names it by. + #[track_caller] + fn read_as_itself(names: &[&'static str], read: fn(&str) -> Option>, name: fn(u8) -> &'static str) { + for &key in names { + let hit = read(key).unwrap_or_else(|| panic!("{key} is not read")); + assert_eq!(hit.tier, Tier::Compat, "{key}"); + assert_eq!(name(hit.slot - COMPAT_SLOTS), key); + } + } + + #[test] + fn each_key_of_each_adapter_is_named_by_its_slot() { + read_as_itself(&fern::definition::NAMES, definition, definition_name); + read_as_itself(&fern::parameter::NAMES, parameter, parameter_name); + read_as_itself(&fern::schema::NAMES, schema, schema_name); + read_as_itself(&stainless::schema::NAMES, schema, schema_name); + } + + #[test] + fn a_key_of_an_adapter_is_read_only_at_its_level() { + let definitions = fern::definition::NAMES.to_vec(); + let parameters = fern::parameter::NAMES.to_vec(); + let schemas = [&fern::schema::NAMES[..], &stainless::schema::NAMES[..]].concat(); + let levels: [Level; 3] = [ + (&definitions, |key| definition(key).is_some()), + (¶meters, |key| parameter(key).is_some()), + (&schemas, |key| schema(key).is_some()), + ]; + for (own, _) in levels { + for &key in own { + for (names, reads) in levels { + assert_eq!(reads(key), names.contains(&key), "{key}"); + } + } + } + } + + #[test] + fn no_two_keys_say_the_same_thing_with_the_same_tier() { + // As `link` reads them: native first, then the adapters. + let keys = |native: &[&'static str], adapters: &[&[&'static str]]| -> Vec<&'static str> { + native + .iter() + .chain(adapters.iter().flat_map(|keys| keys.iter())) + .copied() + .collect() + }; + let definitions = keys(&metadata::definition::NAMES, &[&fern::definition::NAMES]); + let parameters = keys(&metadata::parameter::NAMES, &[&fern::parameter::NAMES]); + let schemas = keys( + &metadata::schema::NAMES, + &[&fern::schema::NAMES, &stainless::schema::NAMES], + ); + + metadata::assert_one_key_per_tier(&definitions, |key| { + metadata::definition::read(key).or_else(|| definition(key)) + }); + metadata::assert_one_key_per_tier(¶meters, |key| { + metadata::parameter::read(key).or_else(|| parameter(key)) + }); + metadata::assert_one_key_per_tier(&schemas, |key| metadata::schema::read(key).or_else(|| schema(key))); + } + + #[test] + fn the_slots_of_the_adapters_do_not_collide_and_fit_in_a_set() { + let mut slots: Vec = fern::schema::NAMES + .iter() + .chain(&stainless::schema::NAMES) + .map(|key| schema(key).unwrap().slot) + .collect(); + let len = slots.len(); + slots.sort_unstable(); + slots.dedup(); + assert_eq!(slots.len(), len); + assert!(slots.iter().all(|&slot| slot < 64)); + } +} diff --git a/src/compat/stainless.rs b/src/compat/stainless.rs new file mode 100644 index 000000000..275ed5b8b --- /dev/null +++ b/src/compat/stainless.rs @@ -0,0 +1,58 @@ +//! The adapter for Stainless's `x-stainless-*` extensions. +//! +//! `x-stainless-skip` may be a list of the targets to skip a property for, +//! which counts as `false`. +//! +//! Its other extensions, such as `x-stainless-terraform-configurability` and +//! `x-stainless-deprecation-message`, are not read: they say things forge has +//! no primitive for. + +/// What every key of the adapter begins with. A key without it is turned +/// away before it is matched against the table. +const PREFIX: &str = "x-stainless-"; + +/// The keys of a Schema Object. +pub(super) mod schema { + use super::PREFIX; + use crate::metadata::{BoolFormat, Hit, SchemaField}; + + const SKIP: &str = "x-stainless-skip"; + + /// The names of the adapter's keys of a Schema Object, by slot. + pub(crate) const NAMES: [&str; 1] = [SKIP]; + + pub(crate) fn read(key: &str) -> Option> { + if !key.starts_with(PREFIX) { + return None; + } + Some(match key { + SKIP => Hit::compat(SchemaField::Skip(BoolFormat::BoolOrTargets), 0), + _ => return None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metadata::{BoolFormat, SchemaField, Tier}; + + #[test] + fn the_skip_is_read_on_a_schema_as_a_boolean_or_targets_and_not_native() { + let key = "x-stainless-skip"; + let hit = schema::read(key).unwrap(); + assert_eq!(hit.tier, Tier::Compat, "{key}"); + assert!(matches!(hit.field, SchemaField::Skip(BoolFormat::BoolOrTargets))); + } + + #[test] + fn other_keys_are_not_read() { + for key in [ + "x-stainless-terraform-configurability", + "x-stainless-deprecation-message", + "x-fern-ignore", + ] { + assert!(schema::read(key).is_none(), "{key}"); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 55e1611d5..1274ce41e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,11 @@ use std::{iter, ptr}; mod arena; +#[cfg(feature = "compat")] +mod compat; mod json; mod link; +mod metadata; mod method; mod name; mod plugin; @@ -223,7 +226,7 @@ impl std::fmt::Debug for Group<'_> { /// own group and name, with the `x-forge-aliases` extension: an array of /// objects, each an alias that is a method of the operation. The /// operation's own group and name then make no method, and its own -/// `x-fern-ignore` does not stop its aliases. An empty array makes no +/// `x-forge-skip` does not stop its aliases. An empty array makes no /// method, an element that is not an object is ignored, and a value that /// is not an array is an error. Aliases do not nest: an `x-forge-aliases` /// in an alias is ignored, as are its keys that are not extensions. @@ -233,7 +236,7 @@ impl std::fmt::Debug for Group<'_> { /// [`availability`](Method::availability), whether it is /// [hidden](Method::is_hidden), its [`confirmation`](Method::confirmation) /// and its [`extensions`](Method::extensions), and whether it is ignored -/// with `x-fern-ignore`. None of these come from the operation: an alias +/// with `x-forge-skip`. None of these come from the operation: an alias /// without an extension does not have the operation's. The rest, from the /// HTTP method and path to the responses, is the operation's, and every /// alias has it. Each alias is a method of its own group, so a model one @@ -250,7 +253,7 @@ pub struct Method<'f> { impl<'f> Method<'f> { /// Returns the name of the method, which is unique within its group. /// - /// Corresponds to the `x-fern-sdk-method-name` extension of the + /// Corresponds to the `x-forge-method-name` extension of the /// operation, or of the alias the method comes from. An operation or /// alias without one, or with an empty one, is not a method, and one /// with no letters or digits is an error. Two methods in a group with @@ -261,12 +264,13 @@ impl<'f> Method<'f> { /// Returns the group the method is in. /// - /// Corresponds to the `x-fern-sdk-group-name` extension of the - /// operation, or of the alias the method comes from: a string of group - /// names separated by dots, such as `accounts.members`, or an array - /// with one group name per element, top level first. An operation or - /// alias without one, with an empty one, or with `x-fern-ignore: true` - /// is not a method. A group name with no letters or digits is an error. + /// Corresponds to the `x-forge-group-name` extension of the operation, or + /// of the alias the method comes from: a string of group names + /// separated by dots, such as `accounts.members`, or an array with one + /// group name per element, top level first, whose elements are not + /// split. An operation or alias without one, with an empty one, or + /// with `x-forge-skip: true` is not a method. A group name with no + /// letters or digits is an error. pub fn group(self) -> Group<'f> { self.forge.group(self.data().group) } @@ -314,7 +318,7 @@ impl<'f> Method<'f> { /// Returns the stage of the API the method is in, such as /// [`Availability::Beta`], or `None` if the operation does not say. /// - /// Corresponds to the `x-fern-availability` extension of the + /// Corresponds to the `x-forge-availability` extension of the /// operation, a string that is one of `alpha`, `beta`, `preview`, /// `generally-available` and `deprecated`, as [`Availability`] lists. /// Any other value, including one that differs only in case, is an @@ -323,7 +327,7 @@ impl<'f> Method<'f> { /// /// It is apart from [`is_deprecated`](Method::is_deprecated), which /// comes from OpenAPI's own `deprecated`: an operation with - /// `x-fern-availability: deprecated` and without `deprecated: true` is + /// `x-forge-availability: deprecated` and without `deprecated: true` is /// [`Availability::Deprecated`] but not deprecated, and one with /// `deprecated: true` has whatever availability it says, or none. pub fn availability(self) -> Option { @@ -335,7 +339,7 @@ impl<'f> Method<'f> { /// /// Corresponds to the `x-forge-hidden` extension of the operation, a /// boolean, which is false without it. A value that is not a boolean - /// is an error, as for `x-fern-ignore`. Unlike `x-fern-ignore`, a + /// is an error, as for `x-forge-skip`. Unlike `x-forge-skip`, a /// hidden operation is still a method. pub fn is_hidden(self) -> bool { self.data().hidden @@ -384,7 +388,7 @@ impl<'f> Method<'f> { /// client sends the fields that [`Field::is_read_only`] reports. A /// parameter's field is never read-only. /// - /// A parameter's field is named after the `x-fern-parameter-name` + /// A parameter's field is named after the `x-forge-parameter-name` /// extension of the parameter, or else its wire name. Its description /// is the parameter's, or else its schema's, following `$ref`s, as /// [`Field::description`] says; from OpenAPI 3.1, the `description` of @@ -494,11 +498,13 @@ impl<'f> Method<'f> { /// /// An extension is a key of the /// [Operation Object](https://spec.openapis.org/oas/latest.html#operation-object) - /// that starts with `x-`. Those forge reads, `x-fern-sdk-method-name`, - /// `x-fern-sdk-group-name`, `x-fern-ignore`, `x-fern-availability`, + /// that starts with `x-`. Those forge reads, `x-forge-method-name`, + /// `x-forge-group-name`, `x-forge-skip`, `x-forge-availability`, /// `x-forge-hidden`, `x-forge-require-confirmation` and - /// `x-forge-aliases`, are left out, and other keys forge does not read - /// are not kept. An extension written more than once is there once, + /// `x-forge-aliases`, are left out, and so are the keys the + /// compatibility layer reads in their place, when forge is built with + /// it. Other keys forge does not read are not kept. An extension + /// written more than once is there once, /// with the last value written, in the place of the first, as with /// `JSON.parse`. Extensions of the path item, the parameters, the body, /// the responses and the schemas are not kept. @@ -588,7 +594,7 @@ impl HttpMethod { /// The stage of the API a [`Method`] is in, as /// [`Method::availability`] gives it. /// -/// Corresponds to the value of the `x-fern-availability` extension of an +/// Corresponds to the value of the `x-forge-availability` extension of an /// operation, which each variant names. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Availability { @@ -606,7 +612,7 @@ pub enum Availability { } impl Availability { - /// Returns the availability as `x-fern-availability` writes it, such + /// Returns the availability as `x-forge-availability` writes it, such /// as `generally-available`. pub fn as_str(self) -> &'static str { match self { @@ -646,7 +652,7 @@ impl<'f> Model<'f> { /// Returns the name of the model. /// - /// Corresponds to the `x-fern-type-name` extension of the schema. A + /// Corresponds to the `x-forge-model-name` extension of the schema. A /// schema without one is named after its schema name, leaving out /// `$defs`, so `Tree/$defs/Node` has the name `Tree Node`. pub fn name(self) -> &'f Name { diff --git a/src/link.rs b/src/link.rs index e77314894..f78561134 100644 --- a/src/link.rs +++ b/src/link.rs @@ -8,6 +8,7 @@ mod openapi; mod read; +mod resolve; use std::borrow::Cow; use std::collections::HashMap; @@ -20,13 +21,14 @@ use serde::de::{self, Deserializer}; use crate::arena::Id; use crate::arena::{List, Span, Str, Strings}; use crate::json::Json; +use crate::metadata; use crate::method::{BodyData, ContentData, ResponseData, is_json}; use crate::types::{ ArrayData, DiscriminatorData, EnumData, FieldData, NameId, ObjectData, TypeData, TypeId, UnionData, }; use crate::{ - Availability, EnumValue, Forge, GroupData, GroupId, HttpMethod, Location, MethodData, MethodId, ModelData, ModelId, - Name, Number, Scalar, + EnumValue, Forge, GroupData, GroupId, HttpMethod, Location, MethodData, MethodId, ModelData, ModelId, Name, Number, + Scalar, }; use openapi::{ Commands, ContentEncoding, Definition, Discriminator, Doc, Format, Info, Item, Literal, Operation, Parameter, @@ -543,8 +545,8 @@ impl<'d> Builder<'d> { self.add_method(path, item, operation, &operation.definition, http_method); return; }; - // The operation's own definition makes no method, so its - // `x-fern-ignore` does not stop its aliases. + // The operation's own definition makes no method, so its being + // ignored does not stop its aliases. for (i, alias) in doc.list(aliases).iter().enumerate() { self.context = Context::Alias(method, path, i); self.add_method(path, item, operation, alias, http_method.clone()); @@ -563,35 +565,42 @@ impl<'d> Builder<'d> { http_method: HttpMethod, ) { let doc = self.doc; - if definition.ignore { + if definition.skip.value { return; } - if definition.method_name.is_empty() { + if definition.method_name.value.is_empty() { return; } - let group_names = doc.list(definition.group_names); + let group_names = doc.list(definition.group_names.value); if group_names.is_empty() { return; } let group_names: Vec = group_names.iter().map(|&name| Name::new(doc.str(name))).collect(); for name in &group_names { if name.is_empty() { - self.error("x-fern-sdk-group-name has a group name with no letters or digits"); + self.error(format_args!( + "{} has a group name with no letters or digits", + resolve::definition_name(definition.group_names.slot) + )); return; } } - let name = Name::new(doc.str(definition.method_name)); + let name = Name::new(doc.str(definition.method_name.value)); if name.is_empty() { - self.error("x-fern-sdk-method-name has no letters or digits"); + self.error(format_args!( + "{} has no letters or digits", + resolve::definition_name(definition.method_name.slot) + )); return; } let mut availability = None; - if let Some(value) = definition.availability { + if let Some(value) = definition.availability.value { let value = doc.str(value); - availability = parse_availability(value); + availability = metadata::availability(value); if availability.is_none() { self.error(format_args!( - "x-fern-availability {value:?} is not alpha, beta, preview, generally-available or deprecated" + "{} {value:?} is not alpha, beta, preview, generally-available or deprecated", + resolve::definition_name(definition.availability.slot) )); } } @@ -950,7 +959,7 @@ impl<'d> Builder<'d> { } /// Returns the name of the field of a property or parameter written as - /// `wire_name`, whose name extension, such as `x-fern-property-name`, + /// `wire_name`, whose name extension, such as `x-forge-property-name`, /// is `extension`. The extension gives the name if it has letters or /// digits. fn field_name(&mut self, extension: Str, wire_name: &'d str) -> NameId { @@ -1267,19 +1276,6 @@ fn fixed_method(method: &str) -> Option { } } -/// Returns the availability `value` is as `x-fern-availability` writes -/// it, such as `beta`, or `None` if it is none of them. Case matters. -fn parse_availability(value: &str) -> Option { - match value { - "alpha" => Some(Availability::Alpha), - "beta" => Some(Availability::Beta), - "preview" => Some(Availability::Preview), - "generally-available" => Some(Availability::GenerallyAvailable), - "deprecated" => Some(Availability::Deprecated), - _ => None, - } -} - /// Reports whether `a` and `b` are the same parameter, so that one replaces /// the other: they have the same location and name. Header names are /// compared ignoring ASCII case, since HTTP field names are @@ -2006,11 +2002,11 @@ impl<'d> Builder<'d> { /// Returns whether `schema` is read-only and sensitive, as a field /// gives them: whether it or any schema along its chain of `$ref`s has - /// `readOnly: true`, and `x-sensitive: true`. + /// `readOnly: true`, and `x-forge-sensitive: true`. /// /// A `readOnly` next to a `$ref` counts from OpenAPI 3.1, and is /// ignored in 3.0, as every keyword next to a `$ref` is. An - /// `x-sensitive` next to a `$ref` counts in every version, since tools + /// `x-forge-sensitive` next to a `$ref` counts in every version, since tools /// commonly honour extensions there. The chain stops at a `$ref` that /// does not point to a schema, which reading the schema's type /// reports, and after 32 `$ref`s, which lead back to themselves. @@ -2162,7 +2158,7 @@ fn item(list: &[Schema], index: &str) -> Option { list.get(index.parse::().ok()?).copied() } -/// Returns the name an extension such as `x-fern-type-name` gives, or +/// Returns the name an extension such as `x-forge-model-name` gives, or /// `None` if there is none, or it has no letters or digits. fn name_of(doc: &Doc, extension: Str) -> Option { if extension.is_empty() { @@ -2212,7 +2208,7 @@ fn decode(segment: &str) -> Cow<'_, str> { Cow::Owned(decoded.replace("~1", "/").replace("~0", "~")) } -/// Returns the name of a model without `x-fern-type-name`: its schema +/// Returns the name of a model without `x-forge-model-name`: its schema /// name without `$defs`. fn model_name(schema_name: &str) -> Name { let mut words = String::with_capacity(schema_name.len()); diff --git a/src/link/openapi.rs b/src/link/openapi.rs index ba7fa4906..14925079c 100644 --- a/src/link/openapi.rs +++ b/src/link/openapi.rs @@ -24,9 +24,11 @@ use serde::Deserialize; use serde::de::{self, DeserializeSeed, Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor}; use super::read::{first, keys}; +use super::resolve; use crate::Name; use crate::arena::{Id, List, Span, Str, Strings}; use crate::json::{self, Json}; +use crate::metadata::{self, BoolFormat, DefinitionField, DocumentField, Hit, ParameterField, SchemaField, Tier}; /// An OpenAPI document: the fields of the OpenAPI Object at its root, and /// the arenas of the objects in it. @@ -236,26 +238,69 @@ pub(super) struct Operation { pub(super) responses: Span<(Str, Id)>, } +/// A value an extension gives, and the key it came from, so that an error can +/// name the key the document wrote. +#[derive(Default)] +pub(super) struct Val { + pub(super) value: T, + /// The slot of the key that gave the value, which names it. It means + /// nothing if no key gave one. + pub(super) slot: u8, +} + +impl Val { + /// Keeps `value`, which `hit` gave, unless a key of a stronger tier + /// than `tier`, the tier of the value held, gave it. + fn set(&mut self, hit: &Hit, value: T, tier: &mut Tier) { + if takes(hit, tier) { + self.value = value; + self.slot = hit.slot; + } + } +} + +/// The tier of the key that gave each value of a [`Definition`] so far, while +/// it is read. +#[derive(Default)] +struct DefinitionTiers { + method_name: Tier, + group_names: Tier, + skip: Tier, + availability: Tier, +} + +/// Reports whether a value from the key of `hit` replaces the one read so +/// far, which came from a key of tier `tier`, and records the tier of the +/// value kept. Equal tiers do not happen: a key written twice is an error +/// before it gets here, and no two keys of one tier say the same thing, +/// which the tests of `metadata` and `compat` check. Were they to, the last +/// written would be kept. +fn takes(hit: &Hit, tier: &mut Tier) -> bool { + if hit.tier >= *tier { + *tier = hit.tier; + return true; + } + false +} + /// The extensions that make an Operation Object, or an alias of one in its /// `x-forge-aliases`, a method. #[derive(Default)] pub(super) struct Definition { - /// The `x-fern-sdk-method-name`, or empty for none. - pub(super) method_name: Str, - /// The `x-fern-sdk-group-name`: the group path, top level first, or - /// empty for none. - pub(super) group_names: Span, - /// `x-fern-ignore`. - pub(super) ignore: bool, - /// `x-forge-hidden`. + /// The name of the method, or empty for none. + pub(super) method_name: Val, + /// The group path, top level first, or empty for none. + pub(super) group_names: Val>, + /// Whether the definition makes no method. + pub(super) skip: Val, + /// Whether the method is left out of listings. pub(super) hidden: bool, - /// The `x-forge-require-confirmation`, or `None` for none. An empty - /// one is `Some`. + /// The sentence to confirm with, or `None` for none. An empty one is + /// `Some`. pub(super) confirmation: Option, - /// The `x-fern-availability` as written, or `None` for none. It is - /// checked as the method is built, so that an error can name the - /// operation. - pub(super) availability: Option, + /// The availability as written, or `None` for none. It is checked as + /// the method is built, so that an error can name the operation. + pub(super) availability: Val>, /// Each other extension, which is not read, in `Doc::extensions`, in /// the order they are written. One written more than once has the last /// value written, in the place of the first. @@ -280,7 +325,7 @@ pub(super) struct Parameter { /// The schema of each media type of a parameter described by /// `content` rather than `schema`, in the order they are written. pub(super) content: Span<(Str, Schema)>, - /// The `x-fern-parameter-name`, or empty for none. + /// The name of the parameter's field, or empty for none. pub(super) field_name: Str, } @@ -396,20 +441,29 @@ pub(super) struct SchemaObject { /// `readOnly`: the value appears in responses, and a client should not /// send it. pub(super) read_only: bool, - /// Whether the schema is marked `x-sensitive: true`: the value is a - /// secret, such as a password or a token. + /// Whether the schema is marked `x-forge-sensitive: true`: the value is + /// a secret, such as a password or a token. pub(super) sensitive: bool, - /// The `x-fern-type-name`: the name of the model, for a schema in - /// `components.schemas`, or empty for none. + /// The name of the model, for a schema in `components.schemas`, or + /// empty for none. pub(super) model_name: Str, - /// The `x-fern-property-name`: the name of the field, for a property's - /// schema, or empty for none. + /// The name of the field, for a property's schema, or empty for none. pub(super) field_name: Str, - /// Whether a property's schema is marked `x-stainless-skip: true`. A - /// list of the targets to skip it for is ignored. + /// Whether a property's schema is marked to be left out: it makes no + /// field. pub(super) skip: bool, } +/// The tier of the key that gave each extension value of a [`SchemaObject`] +/// so far, while it is read. +#[derive(Default)] +struct SchemaTiers { + model_name: Tier, + field_name: Tier, + skip: Tier, + sensitive: Tier, +} + /// A value of `type` in a Schema Object. #[derive(Deserialize, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "lowercase")] @@ -851,8 +905,8 @@ impl Read for Types { } } -/// Reads an `x-fern-sdk-group-name`: a string split on dots, or an array of -/// strings that are not split. The empty string is no group. +/// Reads a group path: a string split on dots, or an array of strings that +/// are not split. The empty string is no group. struct GroupNames; impl Read for GroupNames { @@ -886,6 +940,15 @@ impl Read for GroupNames { } } +/// Reads a boolean in the format it is written in: `true` or `false`, and, +/// if the format says so, an array of targets, which is `false`. +fn read_bool<'de, A: MapAccess<'de>>(map: &mut A, format: BoolFormat) -> Result { + match format { + BoolFormat::Bool => map.next_value(), + BoolFormat::BoolOrTargets => Ok(map.next_value::()?.0), + } +} + /// Reads an array of strings. struct SeqOfStrs<'a>(&'a mut Doc); @@ -1036,6 +1099,49 @@ impl<'de> Deserialize<'de> for FirstServer { } } +/// Declares `$name`, a key of an object that has extensions: what `$resolve` +/// says if it is an `x-` key that means something there, and else one of the +/// OpenAPI keys `$keys` declares, or `Other`. The extensions are kept apart +/// from `$keys`, since they have their own way of being written twice. +macro_rules! entry { + ($name:ident, $keys:ident, $field:ty, $resolve:path) => { + enum $name { + Key($keys), + Extension(Hit<$field>), + } + + impl<'de> Deserialize<'de> for $name { + fn deserialize>(deserializer: D) -> Result<$name, D::Error> { + struct EntryVisitor; + + impl Visitor<'_> for EntryVisitor { + type Value = $name; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("a key") + } + + fn visit_str(self, v: &str) -> Result<$name, E> { + // The OpenAPI keys first: most keys of a document are + // one of them, and none is an extension. Of the rest, + // only an `x-` key is asked about, so that a key that + // is skipped costs one compare. + Ok(match $keys::from_key(v) { + $keys::Other if v.starts_with("x-") => match $resolve(v) { + Some(hit) => $name::Extension(hit), + None => $name::Key($keys::Other), + }, + key => $name::Key(key), + }) + } + } + + deserializer.deserialize_identifier(EntryVisitor) + } + } + }; +} + keys! { DocumentKey { Openapi = "openapi", @@ -1043,10 +1149,11 @@ keys! { Servers = "servers", Paths = "paths", Components = "components", - Commands = "x-forge-commands", } } +entry!(DocumentEntry, DocumentKey, DocumentField, resolve::document); + struct DocumentVisitor<'a>(&'a mut Doc); impl<'de> Visitor<'de> for DocumentVisitor<'_> { @@ -1059,7 +1166,18 @@ impl<'de> Visitor<'de> for DocumentVisitor<'_> { fn visit_map>(self, mut map: A) -> Result<(), A::Error> { let doc = self.0; let mut seen = 0; - while let Some(key) = map.next_key::()? { + let mut seen_extensions = 0; + while let Some(entry) = map.next_key::()? { + let key = match entry { + DocumentEntry::Extension(hit) => { + resolve::once(&hit, &mut seen_extensions)?; + match hit.field { + DocumentField::Commands => doc.commands = map.next_value()?, + } + continue; + } + DocumentEntry::Key(key) => key, + }; key.once(&mut seen)?; match key { DocumentKey::Openapi => doc.openapi = map.next_value()?, @@ -1071,7 +1189,6 @@ impl<'de> Visitor<'de> for DocumentVisitor<'_> { DocumentKey::Components => { doc.components = map.next_value_seed(seed::(doc))?; } - DocumentKey::Commands => doc.commands = map.next_value()?, DocumentKey::Other => skip(&mut map)?, } } @@ -1182,38 +1299,38 @@ impl ReadObject for PathItem { } } -keys! { - /// The keys of a [`Definition`], in an Operation Object or an alias. - DefinitionKey { - MethodName = "x-fern-sdk-method-name", - GroupNames = "x-fern-sdk-group-name", - Ignore = "x-fern-ignore", - Hidden = "x-forge-hidden", - Confirmation = "x-forge-require-confirmation", - Availability = "x-fern-availability", - } -} - impl Definition { - /// Reads the value of `key`, the key of the entry `map` is reading. + /// Reads the value of the key `hit` says, the key of the entry `map` is + /// reading. A value of a key that is not native is kept only until a + /// native key gives one. fn read_value<'de, A: MapAccess<'de>>( &mut self, doc: &mut Doc, map: &mut A, - key: DefinitionKey, + hit: Hit, + tiers: &mut DefinitionTiers, ) -> Result<(), A::Error> { - match key { - DefinitionKey::MethodName => self.method_name = map.next_value_seed(seed::(doc))?, - DefinitionKey::GroupNames => self.group_names = map.next_value_seed(seed::(doc))?, - DefinitionKey::Ignore => self.ignore = map.next_value()?, - DefinitionKey::Hidden => self.hidden = map.next_value()?, - DefinitionKey::Confirmation => { - self.confirmation = Some(map.next_value_seed(seed::(doc))?); + match hit.field { + DefinitionField::Method => { + let name = map.next_value_seed(seed::(doc))?; + self.method_name.set(&hit, name, &mut tiers.method_name); } - DefinitionKey::Availability => { - self.availability = Some(map.next_value_seed(seed::(doc))?); + DefinitionField::Group => { + let names = map.next_value_seed(seed::(doc))?; + self.group_names.set(&hit, names, &mut tiers.group_names); + } + DefinitionField::Skip(format) => { + let skip = read_bool(map, format)?; + self.skip.set(&hit, skip, &mut tiers.skip); + } + DefinitionField::Availability => { + let availability = map.next_value_seed(seed::(doc))?; + self.availability.set(&hit, Some(availability), &mut tiers.availability); + } + DefinitionField::Hidden => self.hidden = map.next_value()?, + DefinitionField::Confirmation => { + self.confirmation = Some(map.next_value_seed(seed::(doc))?); } - DefinitionKey::Other => skip(map)?, } Ok(()) } @@ -1232,13 +1349,14 @@ impl ReadObject for Alias { fn read_object<'de, A: MapAccess<'de>>(doc: &mut Doc, mut map: A) -> Result { let mut definition = Definition::default(); let mut seen = 0; + let mut tiers = DefinitionTiers::default(); // As for an operation. let mut extensions = Vec::new(); while let Some(field) = map.next_key::()? { match field { - AliasField::Definition(key) => { - key.once(&mut seen)?; - definition.read_value(doc, &mut map, key)?; + AliasField::Definition(hit) => { + resolve::once(&hit, &mut seen)?; + definition.read_value(doc, &mut map, hit, &mut tiers)?; } AliasField::Extension(name) => json::insert(&mut extensions, name, map.next_value()?), AliasField::Other => skip(&mut map)?, @@ -1346,7 +1464,7 @@ impl<'de> Visitor<'de> for MaybeAlias<'_> { /// which is kept by its name, or any other key, which is skipped, as /// [`Alias`] says. enum AliasField { - Definition(DefinitionKey), + Definition(Hit), Extension(String), Other, } @@ -1363,13 +1481,13 @@ impl<'de> Deserialize<'de> for AliasField { } fn visit_str(self, v: &str) -> Result { - Ok(match DefinitionKey::from_key(v) { - DefinitionKey::Other if !v.starts_with("x-") => AliasField::Other, - DefinitionKey::Other => match OperationKey::from_key(v) { - OperationKey::Aliases => AliasField::Other, - _ => AliasField::Extension(v.to_owned()), - }, - key => AliasField::Definition(key), + if !v.starts_with("x-") { + return Ok(AliasField::Other); + } + Ok(match resolve::definition(v) { + Some(hit) => AliasField::Definition(hit), + None if metadata::is_aliases(v) => AliasField::Other, + None => AliasField::Extension(v.to_owned()), }) } } @@ -1380,7 +1498,6 @@ impl<'de> Deserialize<'de> for AliasField { keys! { OperationKey { - Aliases = "x-forge-aliases", Summary = "summary", Description = "description", Deprecated = "deprecated", @@ -1398,15 +1515,23 @@ impl ReadObject for Operation { let mut operation = Operation::default(); let mut seen = 0; let mut seen_definition = 0; + let mut tiers = DefinitionTiers::default(); // Kept apart until the operation is read, so that an extension // written again can replace the first. Empty, it allocates nothing. let mut extensions = Vec::new(); while let Some(field) = map.next_key::()? { let key = match field { OperationField::Key(key) => key, - OperationField::Definition(key) => { - key.once(&mut seen_definition)?; - operation.definition.read_value(doc, &mut map, key)?; + OperationField::Definition(hit) => { + resolve::once(&hit, &mut seen_definition)?; + operation.definition.read_value(doc, &mut map, hit, &mut tiers)?; + continue; + } + OperationField::Aliases => { + if operation.aliases.is_some() { + return Err(de::Error::duplicate_field(metadata::ALIASES)); + } + operation.aliases = Some(map.next_value_seed(seed::(doc))?); continue; } OperationField::Extension(name) => { @@ -1416,9 +1541,6 @@ impl ReadObject for Operation { }; key.once(&mut seen)?; match key { - OperationKey::Aliases => { - operation.aliases = Some(map.next_value_seed(seed::(doc))?); - } OperationKey::Summary => operation.summary = map.next_value_seed(seed::(doc))?, OperationKey::Description => { operation.description = map.next_value_seed(seed::(doc))?; @@ -1441,11 +1563,12 @@ impl ReadObject for Operation { } } -/// A key of an Operation Object: one of its definition, another that is -/// read, or an extension that is not, which is kept by its name. Any other -/// key is `OperationKey::Other`. +/// A key of an Operation Object: one of its definition, its aliases, +/// another that is read, or an extension that is not, which is kept by its +/// name. Any other key is `OperationKey::Other`. enum OperationField { - Definition(DefinitionKey), + Definition(Hit), + Aliases, Key(OperationKey), Extension(String), } @@ -1462,12 +1585,13 @@ impl<'de> Deserialize<'de> for OperationField { } fn visit_str(self, v: &str) -> Result { - match DefinitionKey::from_key(v) { - DefinitionKey::Other => {} - key => return Ok(OperationField::Definition(key)), - } + // The OpenAPI keys first, as for any other object. Ok(match OperationKey::from_key(v) { - OperationKey::Other if v.starts_with("x-") => OperationField::Extension(v.to_owned()), + OperationKey::Other if v.starts_with("x-") => match resolve::definition(v) { + Some(hit) => OperationField::Definition(hit), + None if metadata::is_aliases(v) => OperationField::Aliases, + None => OperationField::Extension(v.to_owned()), + }, key => OperationField::Key(key), }) } @@ -1489,10 +1613,11 @@ keys! { Explode = "explode", Schema = "schema", Content = "content", - FieldName = "x-fern-parameter-name", } } +entry!(ParameterEntry, ParameterKey, ParameterField, resolve::parameter); + impl ReadObject for Parameter { type Out = Id; const EXPECTING: &'static str = "a Parameter Object"; @@ -1500,7 +1625,24 @@ impl ReadObject for Parameter { fn read_object<'de, A: MapAccess<'de>>(doc: &mut Doc, mut map: A) -> Result, A::Error> { let mut param = Parameter::default(); let mut seen = 0; - while let Some(key) = map.next_key::()? { + let mut seen_extensions = 0; + let mut field_name_tier = Tier::Unset; + while let Some(entry) = map.next_key::()? { + let key = match entry { + ParameterEntry::Extension(hit) => { + resolve::once(&hit, &mut seen_extensions)?; + match hit.field { + ParameterField::Name => { + let name = map.next_value_seed(seed::(doc))?; + if takes(&hit, &mut field_name_tier) { + param.field_name = name; + } + } + } + continue; + } + ParameterEntry::Key(key) => key, + }; key.once(&mut seen)?; match key { ParameterKey::Ref => param.reference = map.next_value_seed(seed::(doc))?, @@ -1517,7 +1659,6 @@ impl ReadObject for Parameter { ParameterKey::Content => { param.content = map.next_value_seed(seed::>(doc))?; } - ParameterKey::FieldName => param.field_name = map.next_value_seed(seed::(doc))?, ParameterKey::Other => skip(&mut map)?, } } @@ -1619,13 +1760,11 @@ keys! { Default = "default", Deprecated = "deprecated", ReadOnly = "readOnly", - Sensitive = "x-sensitive", - ModelName = "x-fern-type-name", - FieldName = "x-fern-property-name", - Skip = "x-stainless-skip", } } +entry!(SchemaEntry, SchemaKey, SchemaField, resolve::schema); + impl ReadObject for SchemaObject { type Out = Id; const EXPECTING: &'static str = "a Schema Object"; @@ -1633,7 +1772,42 @@ impl ReadObject for SchemaObject { fn read_object<'de, A: MapAccess<'de>>(doc: &mut Doc, mut map: A) -> Result, A::Error> { let mut schema = SchemaObject::default(); let mut seen = 0; - while let Some(key) = map.next_key::()? { + let mut seen_extensions = 0; + let mut tiers = SchemaTiers::default(); + while let Some(entry) = map.next_key::()? { + let key = match entry { + SchemaEntry::Extension(hit) => { + resolve::once(&hit, &mut seen_extensions)?; + match hit.field { + SchemaField::ModelName => { + let name = map.next_value_seed(seed::(doc))?; + if takes(&hit, &mut tiers.model_name) { + schema.model_name = name; + } + } + SchemaField::PropertyName => { + let name = map.next_value_seed(seed::(doc))?; + if takes(&hit, &mut tiers.field_name) { + schema.field_name = name; + } + } + SchemaField::Skip(format) => { + let skip = read_bool(&mut map, format)?; + if takes(&hit, &mut tiers.skip) { + schema.skip = skip; + } + } + SchemaField::Sensitive => { + let sensitive = map.next_value()?; + if takes(&hit, &mut tiers.sensitive) { + schema.sensitive = sensitive; + } + } + } + continue; + } + SchemaEntry::Key(key) => key, + }; key.once(&mut seen)?; match key { SchemaKey::Ref => schema.reference = map.next_value_seed(seed::(doc))?, @@ -1676,10 +1850,6 @@ impl ReadObject for SchemaObject { SchemaKey::Default => schema.default = Some(doc.add(map.next_value::()?)), SchemaKey::Deprecated => schema.deprecated = map.next_value()?, SchemaKey::ReadOnly => schema.read_only = map.next_value()?, - SchemaKey::Sensitive => schema.sensitive = map.next_value()?, - SchemaKey::ModelName => schema.model_name = map.next_value_seed(seed::(doc))?, - SchemaKey::FieldName => schema.field_name = map.next_value_seed(seed::(doc))?, - SchemaKey::Skip => schema.skip = map.next_value::()?.0, SchemaKey::Other => skip(&mut map)?, } } @@ -1783,32 +1953,32 @@ impl<'de> Deserialize<'de> for Count { } } -/// An `x-stainless-skip`: `true` or `false`, or a list of the targets to -/// skip for, which is not `true`. -struct StainlessSkip(bool); +/// A boolean that can also be written as a list of targets, which is not +/// `true`: the targets it would hold for are not read. +struct BoolOrTargets(bool); -impl<'de> Deserialize<'de> for StainlessSkip { - fn deserialize>(deserializer: D) -> Result { - struct SkipVisitor; +impl<'de> Deserialize<'de> for BoolOrTargets { + fn deserialize>(deserializer: D) -> Result { + struct TargetsVisitor; - impl<'de> Visitor<'de> for SkipVisitor { - type Value = StainlessSkip; + impl<'de> Visitor<'de> for TargetsVisitor { + type Value = BoolOrTargets; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("a boolean or an array of targets") } - fn visit_bool(self, v: bool) -> Result { - Ok(StainlessSkip(v)) + fn visit_bool(self, v: bool) -> Result { + Ok(BoolOrTargets(v)) } - fn visit_seq>(self, mut seq: A) -> Result { + fn visit_seq>(self, mut seq: A) -> Result { while let Some(IgnoredAny) = seq.next_element()? {} - Ok(StainlessSkip(false)) + Ok(BoolOrTargets(false)) } } - deserializer.deserialize_any(SkipVisitor) + deserializer.deserialize_any(TargetsVisitor) } } diff --git a/src/link/resolve.rs b/src/link/resolve.rs new file mode 100644 index 000000000..2a2fae2bd --- /dev/null +++ b/src/link/resolve.rs @@ -0,0 +1,144 @@ +//! Resolving the `x-` keys of the objects in [`openapi`](super::openapi). +//! +//! A key is native, when [`metadata`] reads it, or else, if forge is built +//! with the `compat` feature, one the compatibility layer reads in its place. +//! That is the whole fallback: native first, and the compatibility layer for +//! a key that is not native. This is the only place the layer is called, so +//! leaving it out takes one cfg arm for each kind of object, here. +//! +//! Only `x-` keys are asked about. Every other key is OpenAPI's own, which +//! `openapi` knows. + +use serde::de; + +use crate::metadata::{self, DefinitionField, DocumentField, Hit, ParameterField, SchemaField}; + +/// Returns what `key` says on an operation or an alias, if it is a key of +/// one. +pub(super) fn definition(key: &str) -> Option> { + metadata::definition::read(key).or_else(|| compat_definition(key)) +} + +/// Returns what `key` says on a Parameter Object, if it is a key of one. +pub(super) fn parameter(key: &str) -> Option> { + metadata::parameter::read(key).or_else(|| compat_parameter(key)) +} + +/// Returns what `key` says on a Schema Object, if it is a key of one. +pub(super) fn schema(key: &str) -> Option> { + metadata::schema::read(key).or_else(|| compat_schema(key)) +} + +/// Returns what `key` says at the root of the document, if it is a key of +/// it. No compatibility key says anything there. +pub(super) fn document(key: &str) -> Option> { + metadata::document::read(key) +} + +/// A kind of object with extensions, which can name its keys by slot. +pub(super) trait Kind { + /// Returns the name of the key in `slot`, as the document writes it. + fn name(slot: u8) -> &'static str; +} + +impl Kind for DefinitionField { + fn name(slot: u8) -> &'static str { + metadata::native_name(&metadata::definition::NAMES, slot).unwrap_or_else(|| compat_definition_name(slot)) + } +} + +impl Kind for ParameterField { + fn name(slot: u8) -> &'static str { + metadata::native_name(&metadata::parameter::NAMES, slot).unwrap_or_else(|| compat_parameter_name(slot)) + } +} + +impl Kind for SchemaField { + fn name(slot: u8) -> &'static str { + metadata::native_name(&metadata::schema::NAMES, slot).unwrap_or_else(|| compat_schema_name(slot)) + } +} + +impl Kind for DocumentField { + fn name(slot: u8) -> &'static str { + metadata::native_name(&metadata::document::NAMES, slot).unwrap_or_default() + } +} + +/// Returns an error if the key of `hit` is in `seen`, which has a bit for +/// each key read so far from the object, and else adds it. A native key and +/// a compatibility key of the same meaning are different keys, so both can +/// be written: the native one's value is kept. +pub(super) fn once(hit: &Hit, seen: &mut u64) -> Result<(), E> { + let bit = 1u64 << hit.slot; + if *seen & bit != 0 { + return Err(E::duplicate_field(F::name(hit.slot))); + } + *seen |= bit; + Ok(()) +} + +#[cfg(feature = "compat")] +fn compat_definition(key: &str) -> Option> { + crate::compat::definition(key) +} + +#[cfg(not(feature = "compat"))] +fn compat_definition(_: &str) -> Option> { + None +} + +#[cfg(feature = "compat")] +fn compat_parameter(key: &str) -> Option> { + crate::compat::parameter(key) +} + +#[cfg(not(feature = "compat"))] +fn compat_parameter(_: &str) -> Option> { + None +} + +#[cfg(feature = "compat")] +fn compat_schema(key: &str) -> Option> { + crate::compat::schema(key) +} + +#[cfg(not(feature = "compat"))] +fn compat_schema(_: &str) -> Option> { + None +} + +#[cfg(feature = "compat")] +fn compat_definition_name(slot: u8) -> &'static str { + crate::compat::definition_name(slot - metadata::COMPAT_SLOTS) +} + +#[cfg(not(feature = "compat"))] +fn compat_definition_name(_: u8) -> &'static str { + "" +} + +#[cfg(feature = "compat")] +fn compat_parameter_name(slot: u8) -> &'static str { + crate::compat::parameter_name(slot - metadata::COMPAT_SLOTS) +} + +#[cfg(not(feature = "compat"))] +fn compat_parameter_name(_: u8) -> &'static str { + "" +} + +#[cfg(feature = "compat")] +fn compat_schema_name(slot: u8) -> &'static str { + crate::compat::schema_name(slot - metadata::COMPAT_SLOTS) +} + +#[cfg(not(feature = "compat"))] +fn compat_schema_name(_: u8) -> &'static str { + "" +} + +/// Returns the name of the key of an operation or an alias in `slot`. +pub(super) fn definition_name(slot: u8) -> &'static str { + DefinitionField::name(slot) +} diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs new file mode 100644 index 000000000..89b680973 --- /dev/null +++ b/src/metadata/mod.rs @@ -0,0 +1,480 @@ +//! The native extensions: the `x-forge-*` keys a document uses to say what a +//! `Forge` is built from, and what each of them means. +//! +//! This is the whole vocabulary, for each kind of object that has any: +//! [`definition`] for an Operation Object or an alias in its +//! `x-forge-aliases`, [`parameter`], [`schema`] and [`document`]. Each reads +//! a key as the document writes it, and says what it is as a [`Hit`], or +//! `None` if the key is not one of them. +//! +//! Nothing here reads a document or allocates: the keys come in as `&str`s, +//! and what goes out is small values, including the formats a key's value is +//! written in, as data. Reading the values, and building a `Forge` from +//! them, is the work of `link`, which asks this module first and a +//! compatibility layer, if forge is built with one, second; so a native key +//! always has the last word, and the compatibility layer can be left out +//! without changing what any native key means. + +use crate::Availability; + +/// The `x-forge-aliases` key of an Operation Object. It is not in +/// [`definition`], since an alias cannot have one. +pub(crate) const ALIASES: &str = "x-forge-aliases"; + +/// What every native key begins with, but the deprecated `x-sensitive`. A key +/// without it is turned away before it is matched against the table: most +/// extensions in a document are no key of any table, and this is the cheap +/// way to say so. +const PREFIX: &str = "x-forge-"; + +/// A key that says something about the object it is on. +/// +/// It is kept to a few bytes, since one is read for every key of every +/// object: the key's name, which only an error needs, is found from its +/// `slot`, in the table of names of its kind of object. +#[derive(Clone, Copy)] +pub(crate) struct Hit { + /// What the key says. + pub(crate) field: F, + /// Which kind of key it is, which decides whose value is kept when + /// several keys say the same thing. + pub(crate) tier: Tier, + /// The bit of the key in a set of the keys read from an object so far, + /// which is how a key written twice is found, and the index of its name. + /// Unique among the keys of one kind of object, native or not: the + /// native keys count up from zero, and the others from [`COMPAT_SLOTS`]. + pub(crate) slot: u8, +} + +/// Which kind of key gave a value, from the weakest to the strongest. When +/// keys of different tiers say the same thing about an object, the value of +/// the strongest is kept, whatever order they are written in. +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub(crate) enum Tier { + /// No key has given a value yet. + #[default] + Unset, + /// A key a compatibility layer reads in place of a native one. + #[cfg_attr(not(any(feature = "compat", test)), expect(dead_code))] + Compat, + /// A native key that is kept working under an old name, in place of + /// the one that replaced it. + Deprecated, + /// A native key, under its current name. + Native, +} + +/// The first slot of the keys that are not native, so that slots never +/// collide with the native ones, which count up from zero. +#[cfg_attr(not(any(feature = "compat", test)), expect(dead_code))] +pub(crate) const COMPAT_SLOTS: u8 = 32; + +impl Hit { + /// Returns a native key, in slot `slot`, which is under 32. + const fn native(field: F, slot: u8) -> Hit { + Hit { + field, + tier: Tier::Native, + slot, + } + } + + /// Returns a deprecated native key, in slot `slot`, which is under 32: + /// an old name of a native key, read only when the current one is not + /// written. + const fn deprecated(field: F, slot: u8) -> Hit { + Hit { + field, + tier: Tier::Deprecated, + slot, + } + } + + /// Returns a key that is not native, which a compatibility layer + /// reads in place of a native one. It is the `n`th of its kind of + /// object, counting from zero, `n` under 32. + #[cfg(feature = "compat")] + pub(crate) const fn compat(field: F, n: u8) -> Hit { + Hit { + field, + tier: Tier::Compat, + slot: COMPAT_SLOTS + n, + } + } + + /// Returns this key moved `n` slots on, for a compatibility layer that + /// puts the keys of several adapters one after another. + #[cfg(feature = "compat")] + pub(crate) fn after(self, n: u8) -> Hit { + Hit { + field: self.field, + tier: self.tier, + slot: self.slot + n, + } + } +} + +/// Returns the name in `names` of the key in `slot`, if it is a native one. +pub(crate) fn native_name(names: &[&'static str], slot: u8) -> Option<&'static str> { + names.get(usize::from(slot)).copied() +} + +/// What a key says on an operation, or on an alias of one. +#[derive(Clone, Copy)] +pub(crate) enum DefinitionField { + /// The name of the method. + Method, + /// The group path of the method: an array of group names, top level + /// first, or one string of them separated by dots. The empty string is + /// no group. + Group, + /// Whether the definition makes no method. + Skip(BoolFormat), + /// The stage of the API the method is in. + Availability, + /// Whether the method is left out of listings. + Hidden, + /// The sentence to show before calling the method. + Confirmation, +} + +/// What a key says on a Parameter Object. +#[derive(Clone, Copy)] +pub(crate) enum ParameterField { + /// The name of the parameter's field. + Name, +} + +/// What a key says on a Schema Object. +#[derive(Clone, Copy)] +pub(crate) enum SchemaField { + /// The name of the model, for a schema in `components.schemas`. + ModelName, + /// The name of a property's field. + PropertyName, + /// Whether the property makes no field. + Skip(BoolFormat), + /// Whether the schema's value is a secret. + Sensitive, +} + +/// What a key says at the root of the document. +#[derive(Clone, Copy)] +pub(crate) enum DocumentField { + /// The descriptions of the groups. + Commands, +} + +/// How the value of a boolean is written. +#[derive(Clone, Copy)] +pub(crate) enum BoolFormat { + /// `true` or `false`. + Bool, + /// `true` or `false`, or an array of targets, which counts as `false`. + #[cfg_attr(not(feature = "compat"), expect(dead_code))] + BoolOrTargets, +} + +/// Returns whether `key` is the `x-forge-aliases` of an operation. +pub(crate) fn is_aliases(key: &str) -> bool { + key == ALIASES +} + +/// The native keys of an operation, or of an alias in its `x-forge-aliases`. +pub(crate) mod definition { + use super::{BoolFormat, DefinitionField, Hit, PREFIX}; + + const METHOD_NAME: &str = "x-forge-method-name"; + const GROUP_NAME: &str = "x-forge-group-name"; + const SKIP: &str = "x-forge-skip"; + const AVAILABILITY: &str = "x-forge-availability"; + const HIDDEN: &str = "x-forge-hidden"; + const CONFIRMATION: &str = "x-forge-require-confirmation"; + + /// The names of the native keys of an operation or an alias, by slot. + pub(crate) const NAMES: [&str; 6] = [METHOD_NAME, GROUP_NAME, SKIP, AVAILABILITY, HIDDEN, CONFIRMATION]; + + /// Returns what `key` says on an operation or an alias, if it is a native + /// key of one. + pub(crate) fn read(key: &str) -> Option> { + if !key.starts_with(PREFIX) { + return None; + } + Some(match key { + METHOD_NAME => Hit::native(DefinitionField::Method, 0), + GROUP_NAME => Hit::native(DefinitionField::Group, 1), + SKIP => Hit::native(DefinitionField::Skip(BoolFormat::Bool), 2), + AVAILABILITY => Hit::native(DefinitionField::Availability, 3), + HIDDEN => Hit::native(DefinitionField::Hidden, 4), + CONFIRMATION => Hit::native(DefinitionField::Confirmation, 5), + _ => return None, + }) + } +} + +/// The native keys of a Parameter Object. +pub(crate) mod parameter { + use super::{Hit, PREFIX, ParameterField}; + + const NAME: &str = "x-forge-parameter-name"; + + /// The names of the native keys of a Parameter Object, by slot. + pub(crate) const NAMES: [&str; 1] = [NAME]; + + /// Returns what `key` says on a Parameter Object, if it is a native key of + /// one. + pub(crate) fn read(key: &str) -> Option> { + if !key.starts_with(PREFIX) { + return None; + } + Some(match key { + NAME => Hit::native(ParameterField::Name, 0), + _ => return None, + }) + } +} + +/// The native keys of a Schema Object. +pub(crate) mod schema { + use super::{BoolFormat, Hit, PREFIX, SchemaField}; + + const MODEL_NAME: &str = "x-forge-model-name"; + const PROPERTY_NAME: &str = "x-forge-property-name"; + const SKIP: &str = "x-forge-skip"; + const SENSITIVE: &str = "x-forge-sensitive"; + /// The old name of `x-forge-sensitive`, from before every native key began + /// with `x-forge-`. Deprecated: it is read only where `x-forge-sensitive` is + /// not written. + const DEPRECATED_SENSITIVE: &str = "x-sensitive"; + + /// The names of the native keys of a Schema Object, by slot. + pub(crate) const NAMES: [&str; 5] = [MODEL_NAME, PROPERTY_NAME, SKIP, SENSITIVE, DEPRECATED_SENSITIVE]; + + /// Returns what `key` says on a Schema Object, if it is a native key of one. + pub(crate) fn read(key: &str) -> Option> { + // `x-sensitive` is the one native key outside the prefix. + if !key.starts_with(PREFIX) && key != DEPRECATED_SENSITIVE { + return None; + } + Some(match key { + MODEL_NAME => Hit::native(SchemaField::ModelName, 0), + PROPERTY_NAME => Hit::native(SchemaField::PropertyName, 1), + SKIP => Hit::native(SchemaField::Skip(BoolFormat::Bool), 2), + SENSITIVE => Hit::native(SchemaField::Sensitive, 3), + DEPRECATED_SENSITIVE => Hit::deprecated(SchemaField::Sensitive, 4), + _ => return None, + }) + } +} + +/// The native keys at the root of the document. +pub(crate) mod document { + use super::{DocumentField, Hit, PREFIX}; + + const COMMANDS: &str = "x-forge-commands"; + + /// The names of the native keys of the document, by slot. + pub(crate) const NAMES: [&str; 1] = [COMMANDS]; + + /// Returns what `key` says at the root of the document, if it is a native + /// key of it. + pub(crate) fn read(key: &str) -> Option> { + if !key.starts_with(PREFIX) { + return None; + } + Some(match key { + COMMANDS => Hit::native(DocumentField::Commands, 0), + _ => return None, + }) + } +} + +/// Returns the availability `value` is, such as `beta`, or `None` if it is +/// none of them. Case matters. +pub(crate) fn availability(value: &str) -> Option { + match value { + "alpha" => Some(Availability::Alpha), + "beta" => Some(Availability::Beta), + "preview" => Some(Availability::Preview), + "generally-available" => Some(Availability::GenerallyAvailable), + "deprecated" => Some(Availability::Deprecated), + _ => None, + } +} + +/// Asserts that no two of `keys` that `read` reads say the same thing with +/// the same tier. Which key's value is kept is decided by tier alone, so two +/// keys of one tier for one thing would make it depend on the order the +/// keys are written in. +#[cfg(test)] +#[track_caller] +pub(crate) fn assert_one_key_per_tier(keys: &[&str], read: impl Fn(&str) -> Option>) { + let mut said: Vec<(std::mem::Discriminant, Tier, &str)> = Vec::new(); + for &key in keys { + let hit = read(key).unwrap_or_else(|| panic!("{key} is not read")); + let what = std::mem::discriminant(&hit.field); + if let Some(&(_, _, other)) = said.iter().find(|&&(w, tier, _)| w == what && tier == hit.tier) { + panic!("{other} and {key} say the same thing with the same tier"); + } + said.push((what, hit.tier, key)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The names of the keys of a level, and whether the level reads a key. + type Level<'a> = (&'a [&'a str], fn(&str) -> bool); + + /// Every native key of each kind of object, in slot order. + fn native_keys() -> [&'static [&'static str]; 4] { + [ + &[ + "x-forge-method-name", + "x-forge-group-name", + "x-forge-skip", + "x-forge-availability", + "x-forge-hidden", + "x-forge-require-confirmation", + ], + &["x-forge-parameter-name"], + &[ + "x-forge-model-name", + "x-forge-property-name", + "x-forge-skip", + "x-forge-sensitive", + "x-sensitive", + ], + &["x-forge-commands"], + ] + } + + /// Asserts that each of `keys` is read by `read` as a native key whose + /// slot names it in `names`. + #[track_caller] + fn read_as_itself(keys: &[&str], names: &[&'static str], read: fn(&str) -> Option>) { + for &key in keys { + let hit = read(key).unwrap_or_else(|| panic!("{key} is not read")); + assert_ne!(hit.tier, Tier::Compat, "{key}"); + assert_eq!(native_name(names, hit.slot), Some(key)); + } + assert_eq!(keys.len(), names.len(), "every name is a key that is read"); + } + + #[test] + fn each_native_key_is_read_as_itself_and_as_native() { + let [definitions, parameters, schemas, documents] = native_keys(); + read_as_itself(definitions, &definition::NAMES, definition::read); + read_as_itself(parameters, ¶meter::NAMES, parameter::read); + read_as_itself(schemas, &schema::NAMES, schema::read); + read_as_itself(documents, &document::NAMES, document::read); + } + + #[test] + fn no_two_native_keys_say_the_same_thing_with_the_same_tier() { + let [definitions, parameters, schemas, documents] = native_keys(); + assert_one_key_per_tier(definitions, definition::read); + assert_one_key_per_tier(parameters, parameter::read); + assert_one_key_per_tier(schemas, schema::read); + assert_one_key_per_tier(documents, document::read); + } + + #[test] + fn a_native_key_is_read_only_at_its_level() { + let levels: [Level; 4] = [ + (&definition::NAMES, |key| definition::read(key).is_some()), + (¶meter::NAMES, |key| parameter::read(key).is_some()), + (&schema::NAMES, |key| schema::read(key).is_some()), + (&document::NAMES, |key| document::read(key).is_some()), + ]; + for (own, _) in levels { + for &key in own { + for (names, reads) in levels { + assert_eq!(reads(key), names.contains(&key), "{key}"); + } + } + } + } + + #[test] + #[should_panic(expected = "x-forge-sensitive and x-forge-sensitive say the same thing with the same tier")] + fn one_key_per_tier_finds_two() { + assert_one_key_per_tier(&["x-forge-sensitive", "x-forge-sensitive"], schema::read); + } + + #[test] + fn x_sensitive_is_a_deprecated_name_of_x_forge_sensitive() { + let (current, old) = ( + schema::read("x-forge-sensitive").unwrap(), + schema::read("x-sensitive").unwrap(), + ); + + assert!(matches!( + (current.field, old.field), + (SchemaField::Sensitive, SchemaField::Sensitive) + )); + assert_eq!((current.tier, old.tier), (Tier::Native, Tier::Deprecated)); + assert!(Tier::Native > Tier::Deprecated && Tier::Deprecated > Tier::Compat && Tier::Compat > Tier::Unset); + } + + #[test] + fn a_slot_past_the_native_ones_has_no_native_name() { + assert_eq!(native_name(&definition::NAMES, COMPAT_SLOTS), None); + } + + #[test] + fn slots_are_unique_among_the_keys_of_an_object_and_under_the_compat_ones() { + let [definitions, _, schemas, _] = native_keys(); + let slots = |keys: &[&str], hit: &dyn Fn(&str) -> u8| { + let mut slots: Vec = keys.iter().map(|key| hit(key)).collect(); + slots.sort_unstable(); + let len = slots.len(); + slots.dedup(); + assert_eq!(slots.len(), len, "duplicate slot among {keys:?}"); + assert!(slots.iter().all(|&slot| slot < 32), "native slots are under 32"); + }; + slots(definitions, &|key| { + definition::read(key).map_or(u8::MAX, |hit| hit.slot) + }); + slots(schemas, &|key| schema::read(key).map_or(u8::MAX, |hit| hit.slot)); + } + + #[test] + fn other_keys_are_not_native() { + for key in [ + "x-other-method-name", + "x-other-skip", + "x-custom", + "summary", + "x-forge-nope", + "x-forge-method", + ] { + assert!(definition::read(key).is_none(), "{key}"); + assert!(parameter::read(key).is_none(), "{key}"); + assert!(schema::read(key).is_none(), "{key}"); + assert!(document::read(key).is_none(), "{key}"); + } + } + + #[test] + fn aliases_are_not_a_definition_key() { + assert!(is_aliases("x-forge-aliases")); + assert!(definition::read("x-forge-aliases").is_none()); + } + + #[test] + fn availability_is_one_of_five_spellings_and_case_matters() { + for (value, availability) in [ + ("alpha", Availability::Alpha), + ("beta", Availability::Beta), + ("preview", Availability::Preview), + ("generally-available", Availability::GenerallyAvailable), + ("deprecated", Availability::Deprecated), + ] { + assert_eq!(super::availability(value), Some(availability)); + } + assert_eq!(super::availability("Beta"), None); + assert_eq!(super::availability("ga"), None); + } +} diff --git a/src/plugin/ts/tests.rs b/src/plugin/ts/tests.rs index f6c095bde..58ad0a549 100644 --- a/src/plugin/ts/tests.rs +++ b/src/plugin/ts/tests.rs @@ -370,11 +370,11 @@ fn models() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": ["pet-store", "list_all"], "x-fern-sdk-method-name": "get", + "/a": {"get": {"x-forge-group-name": ["pet-store", "list_all"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/pet_owner"}}}}}}}, - "/b": {"get": {"x-fern-sdk-group-name": "pet-store", "x-fern-sdk-method-name": "get", + "/b": {"get": {"x-forge-group-name": ["pet-store"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/2fa"}}}}}}}, - "/c": {"get": {"x-fern-sdk-group-name": "other", "x-fern-sdk-method-name": "get", + "/c": {"get": {"x-forge-group-name": ["other"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/2fa"}}}}}}} }, "components": {"schemas": {"pet_owner": {"type": "string"}, "2fa": {"type": "string"}}} diff --git a/src/plugin/wasm.rs b/src/plugin/wasm.rs index 97220a570..6d5729cd7 100644 --- a/src/plugin/wasm.rs +++ b/src/plugin/wasm.rs @@ -143,7 +143,7 @@ interface forge { json: option, } - /// The stage of the API a method is in, from `x-fern-availability`. + /// The stage of the API a method is in, from `x-forge-availability`. enum availability { alpha, beta, preview, generally-available, deprecated, } diff --git a/src/types.rs b/src/types.rs index 34aab173f..06bc6642c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -293,7 +293,7 @@ impl<'f> Object<'f> { /// except in a [`Method::request`](crate::Method::request) and in /// [`Method::headers`](crate::Method::headers). /// - /// A property marked `x-stainless-skip: true`, or whose schema is + /// A property marked `x-forge-skip: true`, or whose schema is /// `false`, has no field. A name in `required` that is not a property is /// ignored. Two fields with equal names are an error. pub fn fields(self) -> impl ExactSizeIterator> { @@ -330,7 +330,7 @@ impl<'f> Field<'f> { /// Returns the name of the field. /// - /// Corresponds to the `x-fern-property-name` extension of the + /// Corresponds to the `x-forge-property-name` extension of the /// property, or its wire name if it has none. pub fn name(self) -> &'f Name { &self.forge.names[self.data.name.index()] @@ -411,17 +411,19 @@ impl<'f> Field<'f> { /// Reports whether the field holds a secret, such as a password or a /// token, which a client should mask when it shows it. /// - /// Corresponds to the `x-sensitive: true` extension on the property's - /// schema, or on any schema along its chain of `$ref`s. Unlike - /// `readOnly`, an `x-sensitive` next to a `$ref` counts in every + /// Corresponds to the `x-forge-sensitive: true` extension on the property's + /// schema, or on any schema along its chain of `$ref`s. Its old name, + /// `x-sensitive`, is deprecated and still read on a schema without + /// `x-forge-sensitive`. Unlike + /// `readOnly`, an `x-forge-sensitive` next to a `$ref` counts in every /// version of OpenAPI, since tools commonly honour extensions there. A /// value that is not a boolean is an error, as it is for a schema's - /// `x-stainless-skip` and an operation's `x-forge-hidden`. + /// `x-forge-skip` and an operation's `x-forge-hidden`. /// /// A field for a parameter, whether in the request or in /// [`Method::headers`](crate::Method::headers), is sensitive when the - /// parameter's schema is, in the same way: `x-sensitive` next to its - /// `$ref` or along its chain. An `x-sensitive` on the Parameter Object + /// parameter's schema is, in the same way: `x-forge-sensitive` next to its + /// `$ref` or along its chain. An `x-forge-sensitive` on the Parameter Object /// itself, rather than its schema, is ignored. The `body` field of a /// request, and a path template placeholder that no parameter /// declares, are never sensitive. @@ -509,7 +511,7 @@ impl<'f> Discriminator<'f> { } /// Returns the name of the property's field: the - /// `x-fern-property-name` of the property in the members, or else its + /// `x-forge-property-name` of the property in the members, or else its /// wire name. pub fn name(self) -> &'f Name { &self.data.name diff --git a/tests/boundary.rs b/tests/boundary.rs new file mode 100644 index 000000000..95b048d48 --- /dev/null +++ b/tests/boundary.rs @@ -0,0 +1,124 @@ +//! The boundary of the compatibility layer: no other tool's extension is named +//! anywhere but in it. +//! +//! The compiler keeps what the layer reads out of reach of the rest of the +//! crate, but it cannot see a name in a doc comment, a match arm or a test +//! document, so this searches for them. It reads the engine, its tests, and +//! the npm package that ships its docs and its WIT, and fails with the file +//! and line of each name it finds outside the layer and its tests. + +use std::fs; +use std::path::{Path, PathBuf}; + +/// What the extensions of each tool forge reads begin with. +const VENDORS: [&str; 2] = ["x-fern", "x-stainless"]; + +/// Returns whether `line` names `vendor`, as written or as an identifier +/// spells it, with `_` for `-`, as in a test named after a key. +fn names(line: &str, vendor: &str) -> bool { + line.contains(vendor) || line.contains(&vendor.replace('-', "_")) +} + +/// Where those names may be: the layer, its tests, and this file, which +/// writes them to look for them. +const ALLOWED: [&str; 3] = ["src/compat/", "tests/compat/", "tests/boundary.rs"]; + +/// What is searched, from the root of the repository. +const SEARCHED: [&str; 3] = ["src", "tests", "packages/forge"]; + +/// The folders that hold what is built or installed, not what is written. +const SKIPPED: [&str; 4] = ["target", "node_modules", "dist", ".git"]; + +/// Adds every file under `dir` to `out`, leaving out the `SKIPPED` folders. +fn files(dir: &Path, out: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let name = path.file_name().and_then(|name| name.to_str()); + if name.is_some_and(|name| SKIPPED.contains(&name)) { + continue; + } + files(&path, out); + } else { + out.push(path); + } + } +} + +/// Returns each file under `SEARCHED` as its path from the root, with `/` +/// between folders, and what it says. A file that is not text is left out. +fn searched() -> Vec<(String, String)> { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut paths = Vec::new(); + for dir in SEARCHED { + files(&root.join(dir), &mut paths); + } + let mut searched = Vec::new(); + for path in paths { + let Ok(text) = fs::read_to_string(&path) else { + continue; + }; + let relative = path + .strip_prefix(root) + .unwrap_or(&path) + .to_string_lossy() + .replace('\\', "/"); + searched.push((relative, text)); + } + searched +} + +#[test] +fn no_extension_of_another_tool_is_named_outside_the_compatibility_layer() { + let mut found = Vec::new(); + for (path, text) in searched() { + if ALLOWED.iter().any(|allowed| path.starts_with(allowed)) { + continue; + } + for (i, line) in text.lines().enumerate() { + if VENDORS.iter().any(|vendor| names(line, vendor)) { + found.push(format!("{path}:{}: {}", i + 1, line.trim())); + } + } + } + + assert!( + found.is_empty(), + "the extensions of other tools are named only in src/compat/ and tests/compat/, but:\n{}", + found.join("\n") + ); +} + +#[test] +fn the_search_covers_the_engine_its_tests_and_the_npm_package() { + let searched = searched(); + let has = |path: &str| searched.iter().any(|(searched, _)| searched == path); + + for path in [ + "src/lib.rs", + "src/link/openapi.rs", + "src/plugin/wasm.rs", + "tests/parse/main.rs", + "packages/forge/src/index.ts", + "packages/forge/plugin/forge.wit", + ] { + assert!(has(path), "{path} is not searched"); + } +} + +#[test] +fn the_search_finds_the_names_it_looks_for_where_they_are_allowed() { + // So that finding none elsewhere means something. + let searched = searched(); + for vendor in VENDORS { + assert!( + searched + .iter() + .any(|(path, text)| path.starts_with("src/compat/") && text.contains(vendor)), + "src/compat/ does not name {vendor}" + ); + } +} diff --git a/tests/cli.rs b/tests/cli.rs index 6b85c5fc4..8926f77f0 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -6,7 +6,7 @@ use std::process::{Command, Output, Stdio}; /// A document with one method, `a.get`. const SPEC: &str = r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok"}}}}} }"#; diff --git a/tests/compat/common.rs b/tests/compat/common.rs new file mode 100644 index 000000000..7899d1d32 --- /dev/null +++ b/tests/compat/common.rs @@ -0,0 +1,105 @@ +//! Helpers to build documents and to compare what two of them make, so that +//! each test is about one thing. + +use forge::{Forge, Type}; + +/// Parses `json` as a whole document. +#[track_caller] +pub fn parse(json: &str) -> Forge { + match serde_json::from_str(json) { + Ok(forge) => forge, + Err(err) => panic!("parse: {err}"), + } +} + +/// Parses `json`, which must fail, and returns the error message. +#[track_caller] +pub fn parse_err(json: &str) -> String { + match serde_json::from_str::(json) { + Ok(_) => panic!("parse: want an error, got a Forge"), + Err(err) => err.to_string(), + } +} + +/// Returns an OpenAPI 3.0.3 document with one operation, `GET /a`, that has +/// the members `keys`, written as the inside of a JSON object, each followed +/// by a comma, such as `"x-forge-method-name": "get",`. +pub fn operation_with(keys: &str) -> String { + format!( + r#"{{"openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{{keys} "responses": {{"204": {{"description": "ok"}}}}}}}}}}}}"# + ) +} + +/// Returns an OpenAPI 3.0.3 document with one method, `a.get`, whose JSON +/// response has the schema `schema`, and `components.schemas` of `schemas`. +pub fn responding_with(schema: &str, schemas: &str) -> String { + format!( + r#"{{"openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{"x-forge-group-name": ["a"], "x-forge-method-name": "get", + "responses": {{"200": {{"description": "ok", "content": {{"application/json": {{"schema": {schema}}}}}}}}}}}}}}}, + "components": {{"schemas": {schemas}}}}}"# + ) +} + +/// Asserts that `a` and `b` build the same `Forge`, as far as `describe` +/// says. +#[track_caller] +pub fn assert_same(a: &str, b: &str) { + let (a, b) = (describe(&parse(a)), describe(&parse(b))); + assert_eq!(a, b, "the documents build different Forges"); +} + +/// Writes what a `Forge` says about every group, method and model, down to +/// the name of each field, so that two `Forge`s that differ in any name, +/// availability or shape differ in what this returns. +pub fn describe(forge: &Forge) -> String { + let mut out = String::new(); + for group in forge.groups() { + let path: Vec = group.path().map(|name| name.snake()).collect(); + out += &format!("group {}\n", path.join(".")); + for method in group.methods() { + let request = method.request().map_or_else(|| "-".to_string(), shape); + let response = method + .responses() + .map(|response| response.json().map_or_else(|| "-".to_string(), |json| shape(json.ty()))) + .collect::>() + .join(","); + out += &format!( + " method {} {} {} availability={:?} hidden={} request={request} response={response}\n", + method.name().snake(), + method.http_method().as_str(), + method.path(), + method.availability().map(|availability| availability.as_str()), + method.is_hidden(), + ); + } + for model in group.models() { + out += &format!(" model {} = {}\n", model.name().snake(), shape(model.ty())); + } + } + for model in forge.models() { + out += &format!("model {} = {}\n", model.name().snake(), shape(model.ty())); + } + out +} + +/// Writes a type, with the wire name and name of every field, and a model by +/// name. +fn shape(ty: Type<'_>) -> String { + match ty { + Type::Scalar(scalar) => format!("{scalar:?}"), + Type::Enum(values) => format!("enum{values:?}"), + Type::Array(array) => format!("[{}]", shape(array.elem())), + Type::Object(object) => { + let fields: Vec = object + .fields() + .map(|field| format!("{}:{}={}", field.wire_name(), field.name().snake(), shape(field.ty()))) + .collect(); + format!("{{{}}}", fields.join(",")) + } + Type::Union(union) => union.members().map(shape).collect::>().join("|"), + Type::Intersection(intersection) => intersection.members().map(shape).collect::>().join("&"), + Type::Model(model) => format!("Model({})", model.name().snake()), + } +} diff --git a/tests/compat/fern.rs b/tests/compat/fern.rs new file mode 100644 index 000000000..7dc3d6998 --- /dev/null +++ b/tests/compat/fern.rs @@ -0,0 +1,284 @@ +//! Fern's `x-fern-*` extensions: each is read as the native extension that +//! says the same, and as nothing else. + +use super::common::{assert_same, describe, operation_with, parse, parse_err, responding_with}; + +#[test] +fn method_group_and_availability_are_read_as_native() { + assert_same( + &operation_with( + r#""x-fern-sdk-group-name": ["accounts"], "x-fern-sdk-method-name": "list", "x-fern-availability": "beta","#, + ), + &operation_with( + r#""x-forge-group-name": ["accounts"], "x-forge-method-name": "list", "x-forge-availability": "beta","#, + ), + ); +} + +#[test] +fn every_availability_is_read() { + for availability in ["alpha", "beta", "preview", "generally-available", "deprecated"] { + assert_same( + &operation_with(&format!( + r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "get", "x-fern-availability": "{availability}","# + )), + &operation_with(&format!( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-availability": "{availability}","# + )), + ); + } +} + +/// As `x-forge-group-name` reads it, which Fern does not: Fern reads the string +/// as one group. +#[test] +fn a_group_written_as_a_string_is_split_on_dots() { + assert_same( + &operation_with(r#""x-fern-sdk-group-name": "a.b.c", "x-fern-sdk-method-name": "get","#), + &operation_with(r#""x-forge-group-name": "a.b.c", "x-forge-method-name": "get","#), + ); +} + +#[test] +fn a_group_written_as_an_array_is_not_split() { + assert_same( + &operation_with(r#""x-fern-sdk-group-name": ["a.b", "c"], "x-fern-sdk-method-name": "get","#), + &operation_with(r#""x-forge-group-name": ["a.b", "c"], "x-forge-method-name": "get","#), + ); +} + +#[test] +fn an_empty_group_string_is_no_group_and_so_no_method() { + let forge = parse(&operation_with( + r#""x-fern-sdk-group-name": "", "x-fern-sdk-method-name": "get","#, + )); + + assert_eq!(forge.groups().count(), 0); +} + +#[test] +fn a_group_that_is_neither_a_string_nor_an_array_is_an_error() { + let err = parse_err(&operation_with( + r#""x-fern-sdk-group-name": 1, "x-fern-sdk-method-name": "get","#, + )); + + assert!(err.contains("a string or an array of strings"), "{err}"); +} + +#[test] +fn ignore_makes_no_method() { + assert_same( + &operation_with(r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "get", "x-fern-ignore": true,"#), + &operation_with(r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-skip": true,"#), + ); + assert_eq!( + parse(&operation_with( + r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "get", "x-fern-ignore": true,"# + )) + .groups() + .count(), + 0 + ); +} + +#[test] +fn ignore_set_to_false_keeps_the_method() { + assert_same( + &operation_with(r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "get", "x-fern-ignore": false,"#), + &operation_with(r#""x-forge-group-name": ["a"], "x-forge-method-name": "get","#), + ); +} + +#[test] +fn a_parameter_name_is_read_as_native() { + let parameters = |key: &str| { + operation_with(&format!( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "list", + "parameters": [{{"name": "q", "in": "query", "{key}": "query", "schema": {{"type": "string"}}}}],"# + )) + }; + + assert_same( + ¶meters("x-fern-parameter-name"), + ¶meters("x-forge-parameter-name"), + ); + assert!(describe(&parse(¶meters("x-fern-parameter-name"))).contains("q:query=")); +} + +#[test] +fn a_property_name_is_read_as_native() { + let schema = |key: &str| { + responding_with( + &format!(r#"{{"type": "object", "properties": {{"owner": {{"type": "string", "{key}": "person"}}}}}}"#), + "{}", + ) + }; + + assert_same(&schema("x-fern-property-name"), &schema("x-forge-property-name")); + assert!(describe(&parse(&schema("x-fern-property-name"))).contains("owner:person=")); +} + +#[test] +fn a_type_name_is_read_as_the_name_of_a_model() { + let schemas = |key: &str| { + responding_with( + r##"{"$ref": "#/components/schemas/not_found"}"##, + &format!(r#"{{"not_found": {{"type": "object", "{key}": "Missing"}}}}"#), + ) + }; + + assert_same(&schemas("x-fern-type-name"), &schemas("x-forge-model-name")); + assert!(describe(&parse(&schemas("x-fern-type-name"))).contains("model missing =")); +} + +#[test] +fn the_keys_of_an_alias_are_read_too() { + let alias = |group: &str, method: &str, availability: &str, ignore: &str| { + operation_with(&format!( + r#""x-forge-aliases": [{{"{group}": {}, "{method}": "pause", "{availability}": "beta", "{ignore}": false}}],"#, + if group.starts_with("x-fern") { + r#""a.b""# + } else { + r#"["a", "b"]"# + }, + )) + }; + + assert_same( + &alias( + "x-fern-sdk-group-name", + "x-fern-sdk-method-name", + "x-fern-availability", + "x-fern-ignore", + ), + &alias( + "x-forge-group-name", + "x-forge-method-name", + "x-forge-availability", + "x-forge-skip", + ), + ); +} + +#[test] +fn a_key_that_is_read_is_not_an_extension_of_the_method() { + let forge = parse(&operation_with( + r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "get", "x-fern-ignore": false, + "x-fern-availability": "beta", "x-a": 1,"#, + )); + let method = forge.groups().next().unwrap().methods().next().unwrap(); + + assert_eq!(method.extensions().map(|(name, _)| name).collect::>(), ["x-a"]); + for key in [ + "x-fern-sdk-group-name", + "x-fern-sdk-method-name", + "x-fern-ignore", + "x-fern-availability", + ] { + assert_eq!(method.extension(key), None, "{key}"); + } +} + +#[test] +fn a_key_that_is_not_read_stays_an_extension_of_the_method() { + let forge = parse(&operation_with( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-fern-audiences": ["internal"], "x-fern-pagination": {"offset": "o"},"#, + )); + let method = forge.groups().next().unwrap().methods().next().unwrap(); + + assert_eq!( + method.extensions().map(|(name, _)| name).collect::>(), + ["x-fern-audiences", "x-fern-pagination"] + ); +} + +#[test] +fn an_extension_on_a_schema_that_is_not_read_has_no_effect() { + let with = responding_with( + r#"{"type": "string", "x-fern-enum": {"a": {"name": "A"}}, "x-fern-audiences": ["x"]}"#, + "{}", + ); + let without = responding_with(r#"{"type": "string"}"#, "{}"); + + assert_same(&with, &without); +} + +#[test] +fn an_error_names_the_key_the_document_wrote() { + let err = parse_err(&operation_with( + r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "get", "x-fern-availability": "ga","#, + )); + assert!(err.contains(r#"GET /a: x-fern-availability "ga" is not"#), "{err}"); + + let err = parse_err(&operation_with( + r#""x-fern-sdk-group-name": "accounts..members", "x-fern-sdk-method-name": "get","#, + )); + assert!( + err.contains("GET /a: x-fern-sdk-group-name has a group name with no letters or digits"), + "{err}" + ); + + let err = parse_err(&operation_with( + r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "-","#, + )); + assert!( + err.contains("GET /a: x-fern-sdk-method-name has no letters or digits"), + "{err}" + ); +} + +#[test] +fn an_error_in_an_alias_names_the_key_the_document_wrote() { + let err = parse_err(&operation_with( + r#""x-forge-aliases": [1, {"x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "pause", "x-fern-availability": "ga"}],"#, + )); + + assert!( + err.contains(r#"x-forge-aliases[1]: x-fern-availability "ga" is not"#), + "{err}" + ); +} + +#[test] +fn a_key_written_twice_is_an_error_naming_it() { + for (keys, key) in [ + (r#""x-fern-ignore": false, "x-fern-ignore": false,"#, "x-fern-ignore"), + ( + r#""x-fern-sdk-method-name": "a", "x-fern-sdk-method-name": "b","#, + "x-fern-sdk-method-name", + ), + ( + r#""x-fern-availability": "beta", "x-fern-availability": "beta","#, + "x-fern-availability", + ), + ( + r#""x-fern-sdk-group-name": ["a"], "x-fern-sdk-group-name": ["b"],"#, + "x-fern-sdk-group-name", + ), + ] { + let err = parse_err(&operation_with(keys)); + + assert!(err.contains(&format!("duplicate field `{key}`")), "{key}: {err}"); + } +} + +#[test] +fn the_vendor_keys_of_a_kind_of_object_are_read_only_there() { + // A name key on an operation, or a method key on a schema, says nothing. + let forge = parse(&operation_with( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-fern-property-name": "p", "x-fern-type-name": "T", + "x-fern-parameter-name": "q","#, + )); + let method = forge.groups().next().unwrap().methods().next().unwrap(); + assert_eq!(method.extensions().count(), 3); + + let schema = responding_with( + r#"{"type": "object", "properties": {"a": {"type": "string", "x-fern-sdk-method-name": "m", "x-fern-ignore": true}}}"#, + "{}", + ); + assert!( + describe(&parse(&schema)).contains("a:a="), + "{}", + describe(&parse(&schema)) + ); +} diff --git a/tests/compat/main.rs b/tests/compat/main.rs new file mode 100644 index 000000000..c44b04ea4 --- /dev/null +++ b/tests/compat/main.rs @@ -0,0 +1,15 @@ +//! Tests for the compatibility layer: the keys other tools write, read as the +//! native extensions say the same things. +//! +//! Each primitive is tested the same way: a document written with a tool's +//! keys must build the same `Forge` as its native twin. The tests that only +//! the compatibility layer can pass, such as a value format native keys do +//! not have, or a key a tool writes that forge does not read, are written out. +//! +//! They only build with the `compat` feature. The suite of `tests/parse`, +//! which has no vendor names in it, runs without it. + +mod common; +mod fern; +mod native_first; +mod stainless; diff --git a/tests/compat/native_first.rs b/tests/compat/native_first.rs new file mode 100644 index 000000000..6296e9601 --- /dev/null +++ b/tests/compat/native_first.rs @@ -0,0 +1,288 @@ +//! A native key is applied, and the compatibility layer is the fallback: when +//! a document has both, the compat value is ignored, whatever order they are +//! written in and whatever the compat value is, so long as it is a value at +//! all. + +use super::common::{assert_same, describe, operation_with, parse, parse_err, responding_with}; + +/// Asserts that every ordering of `native` and `compat`, which are members of +/// an operation, builds what `expected` alone does. +#[track_caller] +fn native_wins(native: &str, compat: &str, expected: &str) { + for keys in [format!("{native} {compat}"), format!("{compat} {native}")] { + assert_same(&operation_with(&keys), &operation_with(expected)); + } +} + +#[test] +fn the_native_method_wins() { + native_wins( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "native","#, + r#""x-fern-sdk-method-name": "vendor","#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "native","#, + ); + assert!( + describe(&parse(&operation_with( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "native", "x-fern-sdk-method-name": "vendor","# + ))) + .contains("method native") + ); +} + +#[test] +fn the_native_group_wins() { + native_wins( + r#""x-forge-method-name": "get", "x-forge-group-name": ["native"],"#, + r#""x-fern-sdk-group-name": "vendor.group","#, + r#""x-forge-method-name": "get", "x-forge-group-name": ["native"],"#, + ); +} + +#[test] +fn the_native_availability_wins_and_an_invalid_compat_one_is_not_checked() { + native_wins( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-availability": "beta","#, + r#""x-fern-availability": "ga","#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-availability": "beta","#, + ); +} + +#[test] +fn a_native_skip_of_false_keeps_the_method_a_compat_one_would_drop() { + native_wins( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-skip": false,"#, + r#""x-fern-ignore": true,"#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get","#, + ); +} + +#[test] +fn a_native_skip_of_true_drops_the_method_a_compat_one_would_keep() { + native_wins( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-skip": true,"#, + r#""x-fern-ignore": false,"#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-skip": true,"#, + ); +} + +#[test] +fn the_native_parameter_name_wins() { + let parameter = |keys: &str| { + operation_with(&format!( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "list", + "parameters": [{{"name": "q", "in": "query", {keys} "schema": {{"type": "string"}}}}],"# + )) + }; + let native = parameter(r#""x-forge-parameter-name": "native","#); + + for keys in [ + r#""x-forge-parameter-name": "native", "x-fern-parameter-name": "vendor","#, + r#""x-fern-parameter-name": "vendor", "x-forge-parameter-name": "native","#, + ] { + assert_same(¶meter(keys), &native); + } + assert!(describe(&parse(&native)).contains("q:native=")); +} + +#[test] +fn the_native_property_and_model_names_win() { + let schema = |keys: &str| { + responding_with( + &format!(r#"{{"type": "object", "properties": {{"owner": {{"type": "string", {keys}}}}}}}"#), + "{}", + ) + }; + let native = schema(r#""x-forge-property-name": "native""#); + + for keys in [ + r#""x-forge-property-name": "native", "x-fern-property-name": "vendor""#, + r#""x-fern-property-name": "vendor", "x-forge-property-name": "native""#, + ] { + assert_same(&schema(keys), &native); + } + + let model = |keys: &str| { + responding_with( + r##"{"$ref": "#/components/schemas/m"}"##, + &format!(r#"{{"m": {{"type": "object", {keys}}}}}"#), + ) + }; + for keys in [ + r#""x-forge-model-name": "Native", "x-fern-type-name": "Vendor""#, + r#""x-fern-type-name": "Vendor", "x-forge-model-name": "Native""#, + ] { + assert_same(&model(keys), &model(r#""x-forge-model-name": "Native""#)); + } +} + +#[test] +fn the_native_skip_of_a_property_wins_over_a_stainless_one() { + let property = |keys: &str| { + responding_with( + &format!(r#"{{"type": "object", "properties": {{"a": {{"type": "string", {keys}}}}}}}"#), + "{}", + ) + }; + for keys in [ + r#""x-forge-skip": false, "x-stainless-skip": true"#, + r#""x-stainless-skip": true, "x-forge-skip": false"#, + ] { + assert!(describe(&parse(&property(keys))).contains("a:a="), "{keys}"); + } + for keys in [ + r#""x-forge-skip": true, "x-stainless-skip": false"#, + r#""x-stainless-skip": false, "x-forge-skip": true"#, + ] { + assert!(!describe(&parse(&property(keys))).contains("a:a="), "{keys}"); + } +} + +#[test] +fn both_keys_with_the_same_value_are_the_same_as_one() { + assert_same( + &operation_with( + r#""x-forge-group-name": ["a", "b"], "x-fern-sdk-group-name": "a.b", + "x-forge-method-name": "get", "x-fern-sdk-method-name": "get", + "x-forge-availability": "beta", "x-fern-availability": "beta","#, + ), + &operation_with( + r#""x-forge-group-name": ["a", "b"], "x-forge-method-name": "get", "x-forge-availability": "beta","#, + ), + ); +} + +#[test] +fn each_primitive_is_resolved_on_its_own() { + // The group is native and the method and availability are not. + assert_same( + &operation_with( + r#""x-forge-group-name": ["a"], "x-fern-sdk-method-name": "get", "x-fern-availability": "beta","#, + ), + &operation_with( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-availability": "beta","#, + ), + ); +} + +#[test] +fn a_compat_value_of_the_wrong_type_is_an_error_even_when_a_native_key_wins() { + for keys in [ + r#""x-forge-availability": "beta", "x-fern-availability": 1,"#, + r#""x-fern-availability": 1, "x-forge-availability": "beta","#, + ] { + let err = parse_err(&operation_with(&format!( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", {keys}"# + ))); + + assert!(err.contains("invalid type"), "{keys}: {err}"); + } +} + +#[test] +fn a_native_key_written_twice_is_an_error_naming_it() { + let err = parse_err(&operation_with(r#""x-forge-skip": false, "x-forge-skip": false,"#)); + + assert!(err.contains("duplicate field `x-forge-skip`"), "{err}"); +} + +#[test] +fn the_native_group_written_as_a_string_wins() { + native_wins( + r#""x-forge-method-name": "get", "x-forge-group-name": "native.group","#, + r#""x-fern-sdk-group-name": ["vendor"],"#, + r#""x-forge-method-name": "get", "x-forge-group-name": ["native", "group"],"#, + ); +} + +#[test] +fn an_error_names_the_native_key_when_the_native_key_gave_the_value() { + let err = parse_err(&operation_with( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-availability": "ga","#, + )); + + assert!(err.contains(r#"GET /a: x-forge-availability "ga" is not"#), "{err}"); +} + +#[test] +fn only_the_winning_group_and_method_name_are_checked() { + // Each compat value is an error on its own, so the document parsing at + // all shows it was not checked. + native_wins( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get","#, + r#""x-fern-sdk-group-name": "a..b","#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get","#, + ); + native_wins( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get","#, + r#""x-fern-sdk-method-name": "-","#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get","#, + ); + assert!( + parse_err(&operation_with( + r#""x-fern-sdk-group-name": "a..b", "x-forge-method-name": "get","# + )) + .contains("x-fern-sdk-group-name") + ); + assert!( + parse_err(&operation_with( + r#""x-forge-group-name": ["a"], "x-fern-sdk-method-name": "-","# + )) + .contains("x-fern-sdk-method-name") + ); +} + +/// Asserts that every ordering of `native` and `compat`, which are members of +/// an alias, builds what an alias of `expected` alone does, and that it has a +/// method named `method`. +#[track_caller] +fn native_wins_in_an_alias(native: &str, compat: &str, expected: &str, method: &str) { + let alias = |keys: &str| operation_with(&format!(r#""x-forge-aliases": [{{{keys}}}],"#)); + for keys in [format!("{native}, {compat}"), format!("{compat}, {native}")] { + assert_same(&alias(&keys), &alias(expected)); + } + let described = describe(&parse(&alias(expected))); + assert!(described.contains(&format!("method {method} ")), "{described}"); +} + +#[test] +fn the_native_keys_of_an_alias_win() { + native_wins_in_an_alias( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "native""#, + r#""x-fern-sdk-method-name": "vendor""#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "native""#, + "native", + ); + native_wins_in_an_alias( + r#""x-forge-method-name": "get", "x-forge-group-name": ["native"]"#, + r#""x-fern-sdk-group-name": "vendor.group""#, + r#""x-forge-method-name": "get", "x-forge-group-name": ["native"]"#, + "get", + ); + native_wins_in_an_alias( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-availability": "beta""#, + r#""x-fern-availability": "ga""#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-availability": "beta""#, + "get", + ); + native_wins_in_an_alias( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-forge-skip": false"#, + r#""x-fern-ignore": true"#, + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get""#, + "get", + ); +} + +#[test] +fn a_native_skip_in_an_alias_drops_the_method_a_compat_one_would_keep() { + let alias = |keys: &str| { + operation_with(&format!( + r#""x-forge-aliases": [{{"x-forge-group-name": ["a"], "x-forge-method-name": "get", {keys}}}],"# + )) + }; + for keys in [ + r#""x-forge-skip": true, "x-fern-ignore": false"#, + r#""x-fern-ignore": false, "x-forge-skip": true"#, + ] { + assert_eq!(parse(&alias(keys)).groups().count(), 0, "{keys}"); + } +} diff --git a/tests/compat/stainless.rs b/tests/compat/stainless.rs new file mode 100644 index 000000000..d778ad997 --- /dev/null +++ b/tests/compat/stainless.rs @@ -0,0 +1,93 @@ +//! Stainless's `x-stainless-skip`: a property that makes no field, written as +//! a boolean or as a list of targets. + +use super::common::{assert_same, describe, operation_with, parse, parse_err, responding_with}; + +fn with_stainless_skip(value: &str) -> String { + responding_with( + &format!( + r#"{{"type": "object", "properties": {{"id": {{"type": "string"}}, "old_id": {{"type": "string", "x-stainless-skip": {value}}}}}}}"# + ), + "{}", + ) +} + +fn with_native_skip(value: &str) -> String { + responding_with( + &format!( + r#"{{"type": "object", "properties": {{"id": {{"type": "string"}}, "old_id": {{"type": "string", "x-forge-skip": {value}}}}}}}"# + ), + "{}", + ) +} + +#[test] +fn a_stainless_skip_is_read_as_a_native_one() { + assert_same(&with_stainless_skip("true"), &with_native_skip("true")); + assert!(!describe(&parse(&with_stainless_skip("true"))).contains("old_id")); +} + +#[test] +fn a_property_skipped_as_false_keeps_its_field() { + assert_same(&with_stainless_skip("false"), &with_native_skip("false")); + assert!(describe(&parse(&with_stainless_skip("false"))).contains("old_id")); +} + +#[test] +fn a_list_of_targets_is_not_a_skip() { + assert_same( + &with_stainless_skip(r#"["typescript", "go"]"#), + &with_native_skip("false"), + ); + assert!(describe(&parse(&with_stainless_skip(r#"["typescript"]"#))).contains("old_id")); + assert!(describe(&parse(&with_stainless_skip("[]"))).contains("old_id")); +} + +#[test] +fn a_skip_that_is_not_a_boolean_or_a_list_is_an_error() { + let err = parse_err(&with_stainless_skip(r#""yes""#)); + + assert!(err.contains("a boolean or an array of targets"), "{err}"); +} + +#[test] +fn a_list_of_targets_is_an_error_for_the_native_key() { + let err = parse_err(&with_native_skip(r#"["typescript"]"#)); + + assert!(err.contains("invalid type"), "{err}"); +} + +#[test] +fn a_skip_written_twice_is_an_error_naming_it() { + let schema = responding_with( + r#"{"type": "object", "properties": {"a": {"type": "string", "x-stainless-skip": false, "x-stainless-skip": false}}}"#, + "{}", + ); + + assert!(parse_err(&schema).contains("duplicate field `x-stainless-skip`")); +} + +#[test] +fn a_skip_on_an_operation_says_nothing_and_stays_an_extension() { + let forge = parse(&operation_with( + r#""x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-stainless-skip": true,"#, + )); + let method = forge.groups().next().unwrap().methods().next().unwrap(); + + assert_eq!( + method.extensions().map(|(name, _)| name).collect::>(), + ["x-stainless-skip"] + ); +} + +#[test] +fn the_other_stainless_keys_are_not_read() { + let with = responding_with( + r#"{"type": "object", "x-stainless-terraform-configurability": "computed", + "properties": {"a": {"type": "string", "x-stainless-deprecation-message": "gone", "x-stainless-collection-type": "set"}}}"#, + "{}", + ); + let without = responding_with(r#"{"type": "object", "properties": {"a": {"type": "string"}}}"#, "{}"); + + assert_same(&with, &without); +} diff --git a/tests/parse/aliases.rs b/tests/parse/aliases.rs index eb299ef5f..8ff74cbb1 100644 --- a/tests/parse/aliases.rs +++ b/tests/parse/aliases.rs @@ -10,7 +10,7 @@ fn aliased(aliases: &str, fields: &str) -> String { r#"{{ "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, "paths": {{"/a/{{id}}/status": {{"patch": {{{fields} - "x-fern-sdk-group-name": ["a", "status"], "x-fern-sdk-method-name": "edit", + "x-forge-group-name": ["a", "status"], "x-forge-method-name": "edit", "x-forge-aliases": {aliases}, "summary": "Edit status", "description": "Changes the status.", "parameters": [{{"name": "id", "in": "path", "required": true, "schema": {{"type": "string"}}}}], @@ -39,8 +39,8 @@ fn methods(forge: &Forge) -> Vec { } const PAUSE_AND_TERMINATE: &str = r#"[ - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause"}, - {"x-fern-sdk-group-name": ["a"], "x-fern-sdk-method-name": "terminate"} + {"x-forge-group-name": ["a"], "x-forge-method-name": "pause"}, + {"x-forge-group-name": ["a"], "x-forge-method-name": "terminate"} ]"#; #[test] @@ -77,11 +77,11 @@ fn aliases_share_the_rest_of_the_operation() { fn each_alias_has_its_own_availability_hidden_confirmation_and_extensions() { let forge = parse(&aliased( r#"[ - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause", - "x-fern-availability": "generally-available", "x-forge-hidden": true, + {"x-forge-group-name": ["a"], "x-forge-method-name": "pause", + "x-forge-availability": "generally-available", "x-forge-hidden": true, "x-forge-params": {"status": {"default": "pause", "hidden": true}}}, - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "terminate", - "x-fern-availability": "beta", "x-forge-require-confirmation": "Sure?", "x-a": 1, "x-b": 2} + {"x-forge-group-name": ["a"], "x-forge-method-name": "terminate", + "x-forge-availability": "beta", "x-forge-require-confirmation": "Sure?", "x-a": 1, "x-b": 2} ]"#, "", )); @@ -111,8 +111,8 @@ fn each_alias_has_its_own_availability_hidden_confirmation_and_extensions() { #[test] fn alias_does_not_fall_back_to_the_operation() { let forge = parse(&aliased( - r#"[{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause"}]"#, - r#""x-fern-availability": "beta", "x-forge-hidden": true, "x-forge-require-confirmation": "Sure?", + r#"[{"x-forge-group-name": ["a"], "x-forge-method-name": "pause"}]"#, + r#""x-forge-availability": "beta", "x-forge-hidden": true, "x-forge-require-confirmation": "Sure?", "x-forge-params": {"status": {}},"#, )); @@ -128,10 +128,10 @@ fn alias_does_not_fall_back_to_the_operation() { fn alias_without_a_group_or_name_is_not_a_method() { let forge = parse(&aliased( r#"[ - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause"}, - {"x-fern-sdk-group-name": "a"}, - {"x-fern-sdk-method-name": "resume"}, - {"x-fern-sdk-group-name": "", "x-fern-sdk-method-name": "restart"} + {"x-forge-group-name": ["a"], "x-forge-method-name": "pause"}, + {"x-forge-group-name": ["a"]}, + {"x-forge-method-name": "resume"}, + {"x-forge-group-name": [], "x-forge-method-name": "restart"} ]"#, "", )); @@ -142,7 +142,7 @@ fn alias_without_a_group_or_name_is_not_a_method() { #[test] fn element_that_is_not_an_object_is_ignored() { let forge = parse(&aliased( - r#"["pause", null, 1, true, ["a"], {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause"}]"#, + r#"["pause", null, 1, true, ["a"], {"x-forge-group-name": ["a"], "x-forge-method-name": "pause"}]"#, "", )); @@ -159,7 +159,7 @@ fn empty_aliases_make_no_method() { #[test] fn aliases_that_are_not_an_array_are_an_error() { let err = parse_err(&aliased( - r#"{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause"}"#, + r#"{"x-forge-group-name": ["a"], "x-forge-method-name": "pause"}"#, "", )); @@ -172,11 +172,11 @@ fn alias_and_another_method_with_equal_names_are_an_error() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "pause", "responses": {"204": {"description": "ok"}}}}, "/b": {"post": {"x-forge-aliases": [ - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "resume"}, - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "Pause"} + {"x-forge-group-name": ["a"], "x-forge-method-name": "resume"}, + {"x-forge-group-name": ["a"], "x-forge-method-name": "Pause"} ], "responses": {"204": {"description": "ok"}}}} } }"#, @@ -192,8 +192,8 @@ fn alias_and_another_method_with_equal_names_are_an_error() { fn two_aliases_with_equal_names_are_an_error() { let err = parse_err(&aliased( r#"[ - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause"}, - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause"} + {"x-forge-group-name": ["a"], "x-forge-method-name": "pause"}, + {"x-forge-group-name": ["a"], "x-forge-method-name": "pause"} ]"#, "", )); @@ -207,12 +207,12 @@ fn two_aliases_with_equal_names_are_an_error() { #[test] fn alias_group_name_without_letters_is_an_error_naming_the_alias() { let err = parse_err(&aliased( - r#"[{"x-fern-sdk-group-name": "a..b", "x-fern-sdk-method-name": "pause"}]"#, + r#"[{"x-forge-group-name": ["a", "", "b"], "x-forge-method-name": "pause"}]"#, "", )); assert!( - err.contains("PATCH /a/{id}/status: x-forge-aliases[0]: x-fern-sdk-group-name"), + err.contains("PATCH /a/{id}/status: x-forge-aliases[0]: x-forge-group-name"), "{err}" ); } @@ -220,12 +220,12 @@ fn alias_group_name_without_letters_is_an_error_naming_the_alias() { #[test] fn alias_availability_is_checked() { let err = parse_err(&aliased( - r#"[1, {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause", "x-fern-availability": "ga"}]"#, + r#"[1, {"x-forge-group-name": ["a"], "x-forge-method-name": "pause", "x-forge-availability": "ga"}]"#, "", )); assert!( - err.contains(r#"x-forge-aliases[1]: x-fern-availability "ga" is not"#), + err.contains(r#"x-forge-aliases[1]: x-forge-availability "ga" is not"#), "{err}" ); } @@ -233,26 +233,26 @@ fn alias_availability_is_checked() { #[test] fn repeated_key_forge_reads_in_an_alias_is_an_error() { let err = parse_err(&aliased( - r#"[{"x-fern-sdk-method-name": "pause", "x-fern-sdk-method-name": "resume"}]"#, + r#"[{"x-forge-method-name": "pause", "x-forge-method-name": "resume"}]"#, "", )); - assert!(err.contains("duplicate field `x-fern-sdk-method-name`"), "{err}"); + assert!(err.contains("duplicate field `x-forge-method-name`"), "{err}"); } #[test] -fn ignored_operation_still_has_its_aliases() { - let forge = parse(&aliased(PAUSE_AND_TERMINATE, r#""x-fern-ignore": true,"#)); +fn skipped_operation_still_has_its_aliases() { + let forge = parse(&aliased(PAUSE_AND_TERMINATE, r#""x-forge-skip": true,"#)); assert_eq!(methods(&forge), ["a.pause", "a.terminate"]); } #[test] -fn ignored_alias_is_not_a_method() { +fn skipped_alias_is_not_a_method() { let forge = parse(&aliased( r#"[ - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause", "x-fern-ignore": true}, - {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "terminate", "x-fern-ignore": false} + {"x-forge-group-name": ["a"], "x-forge-method-name": "pause", "x-forge-skip": true}, + {"x-forge-group-name": ["a"], "x-forge-method-name": "terminate", "x-forge-skip": false} ]"#, "", )); @@ -263,8 +263,8 @@ fn ignored_alias_is_not_a_method() { #[test] fn aliases_are_not_extensions() { let forge = parse(&aliased( - r#"[{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause", "x-a": 1, - "x-forge-aliases": [{"x-fern-sdk-group-name": "b", "x-fern-sdk-method-name": "nested"}]}]"#, + r#"[{"x-forge-group-name": ["a"], "x-forge-method-name": "pause", "x-a": 1, + "x-forge-aliases": [{"x-forge-group-name": ["b"], "x-forge-method-name": "nested"}]}]"#, "", )); @@ -277,7 +277,7 @@ fn aliases_are_not_extensions() { #[test] fn keys_of_an_alias_that_are_not_extensions_are_ignored() { let forge = parse(&aliased( - r#"[{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "pause", + r#"[{"x-forge-group-name": ["a"], "x-forge-method-name": "pause", "summary": "Pause", "deprecated": false, "path": "/b"}]"#, "", )); @@ -293,7 +293,7 @@ fn operation_without_aliases_keeps_its_own_method() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -307,10 +307,10 @@ fn model_used_by_aliases_in_two_top_level_groups_is_at_the_top_level() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/m": {"get": { - "x-fern-sdk-group-name": "a.b", "x-fern-sdk-method-name": "get", + "x-forge-group-name": ["a", "b"], "x-forge-method-name": "get", "x-forge-aliases": [ - {"x-fern-sdk-group-name": "a.b", "x-fern-sdk-method-name": "get"}, - {"x-fern-sdk-group-name": "x", "x-fern-sdk-method-name": "get"} + {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "get"}, + {"x-forge-group-name": ["x"], "x-forge-method-name": "get"} ], "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}}, "components": {"schemas": {"M": {"type": "object", "properties": {"id": {"type": "string"}}}}} @@ -328,10 +328,10 @@ fn model_used_by_aliases_in_one_group_is_in_that_group() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/m": {"get": { - "x-fern-sdk-group-name": "x", "x-fern-sdk-method-name": "get", + "x-forge-group-name": ["x"], "x-forge-method-name": "get", "x-forge-aliases": [ - {"x-fern-sdk-group-name": "a.b", "x-fern-sdk-method-name": "get"}, - {"x-fern-sdk-group-name": "a.c", "x-fern-sdk-method-name": "get"} + {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "get"}, + {"x-forge-group-name": ["a", "c"], "x-forge-method-name": "get"} ], "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}}, "components": {"schemas": {"M": {"type": "object", "properties": {"id": {"type": "string"}}}}} diff --git a/tests/parse/bodies.rs b/tests/parse/bodies.rs index 4b09e2967..296d30b5a 100644 --- a/tests/parse/bodies.rs +++ b/tests/parse/bodies.rs @@ -21,7 +21,7 @@ fn body_has_its_required_and_description() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"required": true, "description": "The pet.", "content": {"application/json": {"schema": {"type": "string"}}}}, "responses": {"204": {"description": "ok"}}}}} @@ -38,7 +38,7 @@ fn body_is_optional_by_default() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"application/json": {"schema": {"type": "string"}}}}, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -52,7 +52,7 @@ fn contents_are_sorted_by_media_type() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": { "text/plain": {"schema": {"type": "string"}}, "multipart/form-data": {"schema": {"type": "object"}}, @@ -73,7 +73,7 @@ fn json_is_application_json_before_other_json_types() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": { "application/geo+json": {"schema": {"type": "object"}}, "application/json": {"schema": {"type": "string"}} @@ -90,7 +90,7 @@ fn json_is_otherwise_the_first_json_type() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"patch": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"patch": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": { "multipart/form-data": {"schema": {"type": "object"}}, "application/scim+json": {"schema": {"type": "object"}}, @@ -111,7 +111,7 @@ fn json_ignores_case_and_parameters() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"Application/JSON; charset=utf-8": {"schema": {"type": "string"}}}}, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -127,7 +127,7 @@ fn multipart_content_is_typed() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"multipart/form-data": {"schema": { "type": "object", "required": ["file"], "properties": { "file": {"type": "string", "format": "binary"}, @@ -151,7 +151,7 @@ fn octet_stream_content_of_a_binary_string_is_binary() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"application/octet-stream": {"schema": { "type": "string", "format": "binary" }}}}, @@ -169,7 +169,7 @@ fn json_ignores_content_that_is_not_json() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": { "multipart/form-data": {"schema": {"type": "object", "properties": {"name": {"type": "string"}}}}, "text/plain": {"schema": {"type": "string"}} @@ -189,7 +189,7 @@ fn content_that_is_not_json_without_a_schema_is_unknown() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"application/octet-stream": {}}}, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -204,7 +204,7 @@ fn content_without_a_schema_is_unknown() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"application/json": {}}}, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -219,7 +219,7 @@ fn body_ref_is_followed() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"$ref": "#/components/requestBodies/Pet"}, "responses": {"204": {"description": "ok"}}}}}, "components": {"requestBodies": { @@ -239,7 +239,7 @@ fn with_body_schema(version: &str, schema: &str, schemas: &str) -> String { format!( r#"{{ "openapi": "{version}", "info": {{"title": "t", "version": "1"}}, - "paths": {{"/a": {{"post": {{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {{"/a": {{"post": {{"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {{"content": {{"application/json": {{"schema": {schema}}}}}}}, "responses": {{"204": {{"description": "ok"}}}}}}}}}}, "components": {{"schemas": {schemas}}} @@ -301,7 +301,7 @@ fn response_content_has_its_schema_description() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": { "schema": {"type": "string", "description": "The name."}}}}}}}} }"#, diff --git a/tests/parse/common.rs b/tests/parse/common.rs index ed8854cd2..901cf07bb 100644 --- a/tests/parse/common.rs +++ b/tests/parse/common.rs @@ -19,7 +19,7 @@ pub fn responding_with(schema: &str) -> String { format!( r#"{{ "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, - "paths": {{"/a": {{"get": {{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {{"/a": {{"get": {{"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {{"200": {{"description": "ok", "content": {{"application/json": {{"schema": {schema}}}}}}}}}}}}}}} }}"# ) @@ -31,7 +31,7 @@ pub fn responding_with_schemas(schema: &str, schemas: &str) -> String { format!( r#"{{ "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, - "paths": {{"/a": {{"get": {{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {{"/a": {{"get": {{"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {{"200": {{"description": "ok", "content": {{"application/json": {{"schema": {schema}}}}}}}}}}}}}}}, "components": {{"schemas": {schemas}}} }}"# diff --git a/tests/parse/document.rs b/tests/parse/document.rs index 282c6744b..cbb9fc30d 100644 --- a/tests/parse/document.rs +++ b/tests/parse/document.rs @@ -103,7 +103,7 @@ fn every_version_of_3_0_3_1_and_3_2_is_read() { let forge = parse(&format!( r#"{{ "openapi": "{version}", "info": {{"title": "t", "version": "1"}}, - "paths": {{"/a": {{"get": {{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {{"/a": {{"get": {{"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# )); @@ -118,7 +118,7 @@ fn errors_are_reported_together() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "servers": [{"url": "https://{region}.example.com"}], - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a..b", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a", "", "b"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); diff --git a/tests/parse/extensions.rs b/tests/parse/extensions.rs index f626036f4..14fbd9eb0 100644 --- a/tests/parse/extensions.rs +++ b/tests/parse/extensions.rs @@ -9,7 +9,7 @@ fn operation_with(fields: &str) -> String { r#"{{ "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, "paths": {{"/a": {{"get": {{{fields} - "x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# ) @@ -71,7 +71,7 @@ fn method_without_extensions_has_none() { let get = method(&forge, "a.get"); assert_eq!(get.extensions().len(), 0); - assert_eq!(get.extension("x-fern-sdk-method-name"), None); + assert_eq!(get.extension("x-forge-method-name"), None); } #[test] @@ -85,24 +85,24 @@ fn keys_that_are_not_extensions_are_not_kept() { #[test] fn extensions_forge_reads_are_not_kept() { - let forge = parse(&operation_with(r#""x-fern-ignore": false, "x-a": 1,"#)); + let forge = parse(&operation_with(r#""x-forge-skip": false, "x-a": 1,"#)); let get = method(&forge, "a.get"); assert_eq!(names(&forge, "a.get"), ["x-a"]); - assert_eq!(get.extension("x-fern-sdk-group-name"), None); - assert_eq!(get.extension("x-fern-ignore"), None); + assert_eq!(get.extension("x-forge-group-name"), None); + assert_eq!(get.extension("x-forge-skip"), None); } #[test] fn availability_hidden_and_confirmation_are_not_kept() { let forge = parse(&operation_with( - r#""x-a": 1, "x-fern-availability": "beta", "x-forge-hidden": true, + r#""x-a": 1, "x-forge-availability": "beta", "x-forge-hidden": true, "x-forge-require-confirmation": "Sure?", "x-b": 2,"#, )); let get = method(&forge, "a.get"); assert_eq!(names(&forge, "a.get"), ["x-a", "x-b"]); - assert_eq!(get.extension("x-fern-availability"), None); + assert_eq!(get.extension("x-forge-availability"), None); assert_eq!(get.extension("x-forge-hidden"), None); assert_eq!(get.extension("x-forge-require-confirmation"), None); } @@ -113,9 +113,9 @@ fn extensions_forge_reads_are_still_read() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-fern-sdk-group-name": ["a", "b"], "x-fern-sdk-method-name": "list", "x-a": 1, + "get": {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "list", "x-a": 1, "responses": {"204": {"description": "ok"}}}, - "post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", "x-fern-ignore": true, + "post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "x-forge-skip": true, "x-a": 1, "responses": {"204": {"description": "ok"}}} }} }"#, @@ -136,9 +136,9 @@ fn repeated_extension_has_the_last_value_in_the_place_of_the_first() { #[test] fn repeated_extension_forge_reads_is_still_an_error() { - let err = parse_err(&operation_with(r#""x-fern-ignore": false, "x-fern-ignore": false,"#)); + let err = parse_err(&operation_with(r#""x-forge-skip": false, "x-forge-skip": false,"#)); - assert!(err.contains("duplicate field `x-fern-ignore`"), "{err}"); + assert!(err.contains("duplicate field `x-forge-skip`"), "{err}"); } #[test] @@ -147,11 +147,11 @@ fn each_method_keeps_its_own_extensions() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", "x-a": "get", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-a": "get", "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "/b": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}, - "/c": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", "x-a": "create", + "/c": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "x-a": "create", "x-b": [], "responses": {"204": {"description": "ok"}}}} } }"#, @@ -170,7 +170,7 @@ fn extensions_outside_the_operation_are_not_kept() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "x-root": 1, - "paths": {"/a": {"x-path-item": 1, "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"x-path-item": 1, "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "q", "in": "query", "x-parameter": 1, "schema": {"type": "string", "x-schema": 1}}], "responses": {"204": {"description": "ok", "x-response": 1}}}}} }"#, @@ -186,11 +186,11 @@ fn operation_of_a_referenced_path_item_keeps_its_extensions() { "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, "paths": { "/a": {"$ref": "#/components/pathItems/A"}, - "/b": {"get": {"x-fern-sdk-group-name": "b", "x-fern-sdk-method-name": "get", "x-b": 2, + "/b": {"get": {"x-forge-group-name": ["b"], "x-forge-method-name": "get", "x-b": 2, "responses": {"204": {"description": "ok"}}}} }, "components": {"pathItems": { - "A": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", "x-a": 1, + "A": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "x-a": 1, "responses": {"204": {"description": "ok"}}}} }} }"##, diff --git a/tests/parse/groups.rs b/tests/parse/groups.rs index 18618c70e..36a8d4b9b 100644 --- a/tests/parse/groups.rs +++ b/tests/parse/groups.rs @@ -7,7 +7,7 @@ fn string_group_name_splits_on_dots() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "accounts.members", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": "accounts.members", "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -16,12 +16,25 @@ fn string_group_name_splits_on_dots() { assert_eq!(members.parent(), Some(group(&forge, "accounts"))); } +#[test] +fn group_name_that_is_neither_a_string_nor_an_array_is_an_error() { + let err = parse_err( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-group-name": 1, "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + assert!(err.contains("a string or an array of strings"), "{err}"); +} + #[test] fn array_group_name_has_a_group_per_element() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": ["accounts", "members"], "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["accounts", "members"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -35,7 +48,7 @@ fn array_element_with_a_dot_is_one_group() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": ["zones.settings"], "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["zones.settings"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -54,9 +67,9 @@ fn groups_with_equal_names_are_one_group() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "abuse-reports", "x-fern-sdk-method-name": "list", + "/a": {"get": {"x-forge-group-name": ["abuse-reports"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-fern-sdk-group-name": ["abuse_reports"], "x-fern-sdk-method-name": "get", + "/b": {"get": {"x-forge-group-name": ["abuse_reports"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}} } }"#, @@ -72,9 +85,9 @@ fn subgroup_and_method_can_share_a_name() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"delete": {"x-fern-sdk-group-name": "members", "x-fern-sdk-method-name": "delete", + "/a": {"delete": {"x-forge-group-name": ["members"], "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}}}, - "/b": {"post": {"x-fern-sdk-group-name": "members.delete", "x-fern-sdk-method-name": "bulk", + "/b": {"post": {"x-forge-group-name": ["members", "delete"], "x-forge-method-name": "bulk", "responses": {"204": {"description": "ok"}}}} } }"#, @@ -89,7 +102,7 @@ fn operation_without_a_group_is_not_a_method() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -99,15 +112,17 @@ fn operation_without_a_group_is_not_a_method() { #[test] fn empty_group_name_is_no_group() { - let forge = parse( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "", "x-fern-sdk-method-name": "get", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - assert_eq!(forge.groups().count(), 0); + for group in [r#""""#, "[]"] { + let forge = parse(&format!( + r#"{{ + "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{"x-forge-group-name": {group}, "x-forge-method-name": "get", + "responses": {{"204": {{"description": "ok"}}}}}}}}}} + }}"# + )); + + assert_eq!(forge.groups().count(), 0, "{group}"); + } } #[test] @@ -115,7 +130,7 @@ fn operation_without_a_method_name_is_not_a_method() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -128,7 +143,7 @@ fn empty_method_name_is_not_a_method() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -137,14 +152,14 @@ fn empty_method_name_is_not_a_method() { } #[test] -fn ignored_operation_is_not_a_method() { +fn skipped_operation_is_not_a_method() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}, - "post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", "x-fern-ignore": true, + "post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "x-forge-skip": true, "responses": {"204": {"description": "ok"}}} }} }"#, @@ -156,15 +171,20 @@ fn ignored_operation_is_not_a_method() { #[test] fn group_name_without_letters_is_an_error() { - let err = parse_err( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "accounts..members", "x-fern-sdk-method-name": "list", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - assert!(err.contains("GET /a"), "{err}"); + for group in [r#"["accounts", "", "members"]"#, r#""accounts..members""#] { + let err = parse_err(&format!( + r#"{{ + "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{"x-forge-group-name": {group}, "x-forge-method-name": "list", + "responses": {{"204": {{"description": "ok"}}}}}}}}}} + }}"# + )); + + assert!( + err.contains("GET /a: x-forge-group-name has a group name"), + "{group}: {err}" + ); + } } #[test] @@ -173,9 +193,9 @@ fn duplicate_method_in_a_group_is_an_error() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "/b": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}} } }"#, @@ -190,9 +210,9 @@ fn method_names_are_equal_as_names() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list_all", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list_all", "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "listAll", + "/b": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "listAll", "responses": {"204": {"description": "ok"}}}} } }"#, @@ -207,11 +227,11 @@ fn groups_are_depth_first() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a.b.c", "x-fern-sdk-method-name": "get", + "/a": {"get": {"x-forge-group-name": ["a", "b", "c"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-fern-sdk-group-name": "x", "x-fern-sdk-method-name": "get", + "/b": {"get": {"x-forge-group-name": ["x"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}, - "/c": {"get": {"x-fern-sdk-group-name": "a.d", "x-fern-sdk-method-name": "get", + "/c": {"get": {"x-forge-group-name": ["a", "d"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}} } }"#, @@ -241,7 +261,7 @@ fn group_description_comes_from_x_forge_commands() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "accounts", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["accounts"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}}, "x-forge-commands": {"accounts": {"description": "Accounts."}} }"#, @@ -255,7 +275,7 @@ fn subgroup_description_is_in_its_parents_command() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "accounts.members", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["accounts", "members"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}}, "x-forge-commands": {"accounts": {"groups": {"members": {"description": "Members."}}}} }"#, @@ -271,8 +291,13 @@ fn with_commands(groups: &[&str], commands: &str) -> String { .iter() .enumerate() .map(|(i, group)| { + let group = group + .split('.') + .map(|name| format!("{name:?}")) + .collect::>() + .join(", "); format!( - r#""/{i}": {{"get": {{"x-fern-sdk-group-name": "{group}", "x-fern-sdk-method-name": "list", + r#""/{i}": {{"get": {{"x-forge-group-name": [{group}], "x-forge-method-name": "list", "responses": {{"204": {{"description": "ok"}}}}}}}}"# ) }) @@ -341,7 +366,7 @@ fn command_matches_a_group_with_an_equal_name() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "abuse_reports", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["abuse_reports"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}}, "x-forge-commands": {"abuse-reports": {"description": "Abuse reports."}} }"#, @@ -355,7 +380,7 @@ fn group_without_a_command_has_no_description() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "accounts", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["accounts"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}}, "x-forge-commands": {"zones": {"description": "Zones."}} }"#, @@ -384,13 +409,13 @@ fn groups_are_sorted_by_name() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "zones", "x-fern-sdk-method-name": "get", + "/a": {"get": {"x-forge-group-name": ["zones"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-fern-sdk-group-name": "accounts.members", "x-fern-sdk-method-name": "get", + "/b": {"get": {"x-forge-group-name": ["accounts", "members"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}, - "/c": {"get": {"x-fern-sdk-group-name": "dns", "x-fern-sdk-method-name": "get", + "/c": {"get": {"x-forge-group-name": ["dns"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}, - "/d": {"get": {"x-fern-sdk-group-name": "accounts.billing", "x-fern-sdk-method-name": "get", + "/d": {"get": {"x-forge-group-name": ["accounts", "billing"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}} } }"#, @@ -412,11 +437,11 @@ fn methods_are_sorted_by_name() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}, - "post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "responses": {"204": {"description": "ok"}}}, - "delete": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "delete", + "delete": {"x-forge-group-name": ["a"], "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}} }} }"#, diff --git a/tests/parse/headers.rs b/tests/parse/headers.rs index 49f34a22a..1d45cdb8c 100644 --- a/tests/parse/headers.rs +++ b/tests/parse/headers.rs @@ -17,7 +17,7 @@ fn header_param_is_a_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "X-Request-ID", "in": "header", "required": true, "description": "Traces the request.", "schema": {"type": "integer", "format": "int64"}}, @@ -39,12 +39,12 @@ fn header_param_is_a_field() { } #[test] -fn header_field_is_named_by_x_fern_parameter_name() { +fn header_field_is_named_by_x_forge_parameter_name() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", - "parameters": [{"name": "cf-r2-jurisdiction", "in": "header", "x-fern-parameter-name": "jurisdiction", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", + "parameters": [{"name": "cf-r2-jurisdiction", "in": "header", "x-forge-parameter-name": "jurisdiction", "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -59,7 +59,7 @@ fn header_description_is_the_params_or_else_its_schemas() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "X-Own", "in": "header", "description": "Param.", "schema": {"$ref": "#/components/schemas/Id"}}, {"name": "X-Inherited", "in": "header", "schema": {"$ref": "#/components/schemas/Id"}}, @@ -81,7 +81,7 @@ fn deprecated_header_is_a_deprecated_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "X-Old", "in": "header", "deprecated": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -95,14 +95,14 @@ fn header_is_sensitive_when_its_schema_is_and_never_read_only() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ - {"name": "X-Api-Key", "in": "header", "schema": {"type": "string", "x-sensitive": true, "readOnly": true}}, + {"name": "X-Api-Key", "in": "header", "schema": {"type": "string", "x-forge-sensitive": true, "readOnly": true}}, {"name": "X-Auth-Token", "in": "header", "schema": {"$ref": "#/components/schemas/Token"}}, {"name": "X-Trace", "in": "header", "schema": {"type": "string"}} ], "responses": {"204": {"description": "ok"}}}}}, - "components": {"schemas": {"Token": {"type": "string", "x-sensitive": true, "readOnly": true}}} + "components": {"schemas": {"Token": {"type": "string", "x-forge-sensitive": true, "readOnly": true}}} }"##, ); @@ -119,7 +119,7 @@ fn header_has_the_default_of_its_schema() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "X-Version", "in": "header", "schema": {"type": "string", "default": "2024-01-01"}}, {"name": "X-Retries", "in": "header", "schema": {"$ref": "#/components/schemas/Retries"}}, @@ -144,7 +144,7 @@ fn header_param_ref_is_followed() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "parameters": [{"$ref": "#/components/parameters/IdempotencyKey"}], "responses": {"204": {"description": "ok"}}}}}, "components": {"parameters": {"IdempotencyKey": {"name": "Idempotency-Key", "in": "header", @@ -163,8 +163,8 @@ fn path_item_headers_belong_to_each_operation() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { "parameters": [{"name": "X-Account", "in": "header", "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", "responses": {"204": {"description": "ok"}}}, - "delete": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "delete", "responses": {"204": {"description": "ok"}}} + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-group-name": ["a"], "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}} }} }"#, ); @@ -181,7 +181,7 @@ fn operation_header_replaces_the_path_item_header() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { "parameters": [{"name": "X-Limit", "in": "header", "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "X-Limit", "in": "header", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}} }} @@ -202,7 +202,7 @@ fn operation_header_replaces_the_path_item_header_ignoring_case() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { "parameters": [{"name": "X-Request-ID", "in": "header", "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "x-request-id", "in": "header", "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}} }} @@ -221,7 +221,7 @@ fn query_param_does_not_replace_a_header_with_its_name() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { "parameters": [{"name": "version", "in": "header", "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "version", "in": "query", "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}} }} @@ -243,7 +243,7 @@ fn headers_are_in_declared_order_the_path_items_first() { {"name": "X-First", "in": "header", "schema": {"type": "string"}}, {"name": "X-Second", "in": "header", "schema": {"type": "string"}} ], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "X-Third", "in": "header", "schema": {"type": "string"}}, {"name": "x-first", "in": "header", "schema": {"type": "string"}} @@ -264,7 +264,7 @@ fn headers_with_equal_names_are_an_error() { let err = parse_err( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "X-Rate-Limit", "in": "header", "schema": {"type": "string"}}, {"name": "X_Rate_Limit", "in": "header", "schema": {"type": "string"}} @@ -289,7 +289,7 @@ fn path_item_headers_differing_in_case_are_an_error() { {"name": "X-A", "in": "header", "schema": {"type": "string"}}, {"name": "x-a", "in": "header", "schema": {"type": "string"}} ], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}} }} }"#, @@ -303,7 +303,7 @@ fn header_and_param_with_equal_names_are_not_an_error() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{account_id}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a/{account_id}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "account_id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "Account-Id", "in": "header", "schema": {"type": "string"}} @@ -320,7 +320,7 @@ fn cookie_params_are_not_headers() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "session", "in": "cookie", "schema": {"type": "string"}}, {"name": "X-Trace", "in": "header", "schema": {"type": "string"}} @@ -337,7 +337,7 @@ fn method_with_only_cookie_params_has_no_headers() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "session", "in": "cookie", "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -351,7 +351,7 @@ fn accept_content_type_and_authorization_headers_are_kept() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "upload", + "paths": {"/a": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "upload", "parameters": [ {"name": "Content-Type", "in": "header", "required": true, "schema": {"type": "string"}}, {"name": "Content-Length", "in": "header", "schema": {"type": "integer"}}, @@ -376,7 +376,7 @@ fn method_without_header_params_has_no_headers() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -391,7 +391,7 @@ fn headers_are_not_in_the_request() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "parameters": [ {"name": "page", "in": "query", "schema": {"type": "integer"}}, {"name": "X-Trace", "in": "header", "schema": {"type": "string"}} @@ -411,7 +411,7 @@ fn method_with_only_headers_has_no_request() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "X-Trace", "in": "header", "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -427,7 +427,7 @@ fn model_used_only_by_a_header_is_a_model() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a.b", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "get", "parameters": [{"name": "cf-r2-storage-class", "in": "header", "schema": {"$ref": "#/components/schemas/StorageClass"}}], "responses": {"204": {"description": "ok"}}}}}, "components": {"schemas": {"StorageClass": {"type": "string", "enum": ["Standard", "InfrequentAccess"]}}} @@ -446,7 +446,7 @@ fn header_with_content_is_an_error() { let err = parse_err( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "X-Filter", "in": "header", "content": {"application/json": {"schema": { "type": "object", "properties": {"status": {"type": "string"}} }}}}], @@ -465,7 +465,7 @@ fn header_style_other_than_simple_is_an_error() { let err = parse_err( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "X-Ids", "in": "header", "style": "form", "schema": {"type": "array", "items": {"type": "string"}}}], "responses": {"204": {"description": "ok"}}}}} diff --git a/tests/parse/methods.rs b/tests/parse/methods.rs index 5daea4ea9..029470219 100644 --- a/tests/parse/methods.rs +++ b/tests/parse/methods.rs @@ -9,7 +9,7 @@ fn operation_with(fields: &str) -> String { r#"{{ "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, "paths": {{"/a": {{"get": {{{fields} - "x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# ) @@ -21,14 +21,14 @@ fn each_operation_field_is_its_http_method() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", "responses": {"204": {"description": "ok"}}}, - "put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "put", "responses": {"204": {"description": "ok"}}}, - "post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "post", "responses": {"204": {"description": "ok"}}}, - "delete": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "delete", "responses": {"204": {"description": "ok"}}}, - "options": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "options", "responses": {"204": {"description": "ok"}}}, - "head": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "head", "responses": {"204": {"description": "ok"}}}, - "patch": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "patch", "responses": {"204": {"description": "ok"}}}, - "trace": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "trace", "responses": {"204": {"description": "ok"}}} + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "put": {"x-forge-group-name": ["a"], "x-forge-method-name": "put", "responses": {"204": {"description": "ok"}}}, + "post": {"x-forge-group-name": ["a"], "x-forge-method-name": "post", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-group-name": ["a"], "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}}, + "options": {"x-forge-group-name": ["a"], "x-forge-method-name": "options", "responses": {"204": {"description": "ok"}}}, + "head": {"x-forge-group-name": ["a"], "x-forge-method-name": "head", "responses": {"204": {"description": "ok"}}}, + "patch": {"x-forge-group-name": ["a"], "x-forge-method-name": "patch", "responses": {"204": {"description": "ok"}}}, + "trace": {"x-forge-group-name": ["a"], "x-forge-method-name": "trace", "responses": {"204": {"description": "ok"}}} }} }"#, ); @@ -54,7 +54,7 @@ fn query_operation_is_query() { let forge = parse( r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"query": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "search", + "paths": {"/a": {"query": {"x-forge-group-name": ["a"], "x-forge-method-name": "search", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -68,7 +68,7 @@ fn additional_operation_is_its_method_as_written() { r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, "paths": {"/a": {"additionalOperations": { - "COPY": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "copy", + "COPY": {"x-forge-group-name": ["a"], "x-forge-method-name": "copy", "responses": {"204": {"description": "ok"}}} }}} }"#, @@ -86,7 +86,7 @@ fn additional_operation_with_the_method_of_a_field_is_that_variant() { r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, "paths": {"/a": {"additionalOperations": { - "GET": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "GET": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}} }}} }"#, @@ -101,7 +101,7 @@ fn additional_operation_methods_are_case_sensitive() { r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, "paths": {"/a": {"additionalOperations": { - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}} }}} }"#, @@ -118,7 +118,7 @@ fn method_has_the_summary_description_and_deprecation_of_its_operation() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "summary": "Get an a.", "description": "Gets an a.", "deprecated": true, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -135,7 +135,7 @@ fn method_without_docs_is_empty_and_not_deprecated() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -153,7 +153,7 @@ fn path_item_ref_is_followed() { "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, "paths": {"/a": {"$ref": "#/components/pathItems/A"}}, "components": {"pathItems": { - "A": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "A": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}} }} }"##, @@ -168,11 +168,11 @@ fn extensions_in_paths_and_path_items_are_not_operations() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "x-paths-extension": {"get": {"x-fern-sdk-group-name": "x", "x-fern-sdk-method-name": "get", + "x-paths-extension": {"get": {"x-forge-group-name": ["x"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}, "/a": { "x-forge-commands": {"a": {"description": "A."}}, - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}} } } @@ -189,9 +189,9 @@ fn additional_operation_with_the_method_of_a_present_field_is_skipped() { r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, - "additionalOperations": {"GET": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "shadowed", + "additionalOperations": {"GET": {"x-forge-group-name": ["a"], "x-forge-method-name": "shadowed", "responses": {"204": {"description": "ok"}}}} }} }"#, @@ -206,7 +206,7 @@ fn webhooks_are_not_methods() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "webhooks": {"newPet": {"post": {"x-fern-sdk-group-name": "pets", "x-fern-sdk-method-name": "created", + "webhooks": {"newPet": {"post": {"x-forge-group-name": ["pets"], "x-forge-method-name": "created", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -219,9 +219,9 @@ fn callbacks_are_not_methods() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/subscribe": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "subscribe", + "paths": {"/subscribe": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "subscribe", "callbacks": {"event": {"{$request.query.url}": {"post": { - "x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "event", + "x-forge-group-name": ["a"], "x-forge-method-name": "event", "responses": {"204": {"description": "ok"}} }}}}, "responses": {"204": {"description": "ok"}}}}} @@ -297,7 +297,7 @@ fn each_availability_is_its_variant() { ("generally-available", Availability::GenerallyAvailable), ("deprecated", Availability::Deprecated), ] { - let forge = parse(&operation_with(&format!(r#""x-fern-availability": "{written}","#))); + let forge = parse(&operation_with(&format!(r#""x-forge-availability": "{written}","#))); assert_eq!(method(&forge, "a.get").availability(), Some(want), "{written}"); assert_eq!(want.as_str(), written); @@ -306,21 +306,21 @@ fn each_availability_is_its_variant() { #[test] fn unknown_availability_is_an_error_naming_the_operation() { - let err = parse_err(&operation_with(r#""x-fern-availability": "stable","#)); + let err = parse_err(&operation_with(r#""x-forge-availability": "stable","#)); - assert!(err.contains(r#"GET /a: x-fern-availability "stable" is not"#), "{err}"); + assert!(err.contains(r#"GET /a: x-forge-availability "stable" is not"#), "{err}"); } #[test] fn availability_is_case_sensitive() { - let err = parse_err(&operation_with(r#""x-fern-availability": "Beta","#)); + let err = parse_err(&operation_with(r#""x-forge-availability": "Beta","#)); - assert!(err.contains(r#"GET /a: x-fern-availability "Beta" is not"#), "{err}"); + assert!(err.contains(r#"GET /a: x-forge-availability "Beta" is not"#), "{err}"); } #[test] fn availability_that_is_not_a_string_is_an_error() { - let err = parse_err(&operation_with(r#""x-fern-availability": 1,"#)); + let err = parse_err(&operation_with(r#""x-forge-availability": 1,"#)); assert!(err.contains("expected a string"), "{err}"); } @@ -331,13 +331,13 @@ fn availability_and_deprecation_are_apart() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", - "deprecated": true, "x-fern-availability": "generally-available", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", + "deprecated": true, "x-forge-availability": "generally-available", "responses": {"204": {"description": "ok"}}}, - "put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "put", - "x-fern-availability": "deprecated", + "put": {"x-forge-group-name": ["a"], "x-forge-method-name": "put", + "x-forge-availability": "deprecated", "responses": {"204": {"description": "ok"}}}, - "post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "post", + "post": {"x-forge-group-name": ["a"], "x-forge-method-name": "post", "deprecated": true, "responses": {"204": {"description": "ok"}}} }} }"#, @@ -353,3 +353,16 @@ fn availability_and_deprecation_are_apart() { assert!(post.is_deprecated()); assert_eq!(post.availability(), None); } + +#[test] +fn x_forge_method_without_name_is_not_a_method_name() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method": "get", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + assert_eq!(forge.groups().count(), 0); +} diff --git a/tests/parse/models.rs b/tests/parse/models.rs index d6373aafe..f4bfe3b36 100644 --- a/tests/parse/models.rs +++ b/tests/parse/models.rs @@ -13,8 +13,8 @@ fn response_model_is_placed_in_its_group() { "paths": { "/zones": { "get": { - "x-fern-sdk-group-name": "zones", - "x-fern-sdk-method-name": "list", + "x-forge-group-name": ["zones"], + "x-forge-method-name": "list", "responses": { "200": { "description": "OK", @@ -43,9 +43,9 @@ fn model_used_in_sibling_groups_is_in_their_parent() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/c": {"get": {"x-fern-sdk-group-name": "a.b.c", "x-fern-sdk-method-name": "get", + "/c": {"get": {"x-forge-group-name": ["a", "b", "c"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/d": {"get": {"x-fern-sdk-group-name": "a.b.d", "x-fern-sdk-method-name": "get", + "/d": {"get": {"x-forge-group-name": ["a", "b", "d"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}} }, "components": {"schemas": {"M": {"type": "object", "properties": {"id": {"type": "string"}}}}} @@ -61,9 +61,9 @@ fn model_used_in_a_group_and_its_subgroup_is_in_the_group() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/c": {"get": {"x-fern-sdk-group-name": "a.b.c", "x-fern-sdk-method-name": "get", + "/c": {"get": {"x-forge-group-name": ["a", "b", "c"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}} }, "components": {"schemas": {"M": {"type": "object", "properties": {"id": {"type": "string"}}}}} @@ -79,9 +79,9 @@ fn model_used_in_top_level_groups_is_at_the_top_level() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/x": {"get": {"x-fern-sdk-group-name": "x", "x-fern-sdk-method-name": "get", + "/x": {"get": {"x-forge-group-name": ["x"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}} }, "components": {"schemas": {"M": {"type": "object", "properties": {"id": {"type": "string"}}}}} @@ -99,9 +99,9 @@ fn model_used_through_another_model_is_placed_by_every_use() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/b": {"get": {"x-fern-sdk-group-name": "a.b", "x-fern-sdk-method-name": "get", + "/b": {"get": {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Outer"}}}}}}}, - "/c": {"get": {"x-fern-sdk-group-name": "a.c", "x-fern-sdk-method-name": "get", + "/c": {"get": {"x-forge-group-name": ["a", "c"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Inner"}}}}}}} }, "components": {"schemas": { @@ -120,7 +120,7 @@ fn params_bodies_and_errors_use_models() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "parameters": [{"name": "filter", "in": "query", "schema": {"$ref": "#/components/schemas/Filter"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Input"}}}}, "responses": {"400": {"description": "no", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Failure"}}}}}}}}, @@ -144,16 +144,16 @@ fn schema_no_method_uses_is_not_a_model() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Used"}}}}}}}, - "/ignored": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "ignored", "x-fern-ignore": true, - "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Ignored"}}}}}}}, - "/ungrouped": {"get": {"x-fern-sdk-method-name": "ungrouped", + "/skipped": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "skipped", "x-forge-skip": true, + "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Skipped"}}}}}}}, + "/ungrouped": {"get": {"x-forge-method-name": "ungrouped", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Ungrouped"}}}}}}} }, "components": {"schemas": { "Used": {"type": "string"}, - "Ignored": {"type": "string"}, + "Skipped": {"type": "string"}, "Ungrouped": {"type": "string"}, "Unused": {"type": "string"} }} @@ -186,10 +186,10 @@ fn model_is_named_after_its_schema() { } #[test] -fn model_is_named_by_x_fern_type_name() { +fn model_is_named_by_x_forge_model_name() { let forge = parse(&responding_with_schemas( r##"{"$ref": "#/components/schemas/not_found"}"##, - r#"{"not_found": {"type": "object", "x-fern-type-name": "Missing"}}"#, + r#"{"not_found": {"type": "object", "x-forge-model-name": "Missing"}}"#, )); assert_eq!(model(&forge, "not_found").name(), &Name::new("Missing")); @@ -224,7 +224,7 @@ fn schema_in_defs_is_a_model() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Tree/$defs/Node"}}}}}}}}, "components": {"schemas": { "Tree": {"type": "object", "$defs": {"Node": {"type": "string"}}} @@ -256,7 +256,7 @@ fn schema_used_only_by_content_that_is_not_json_is_a_model() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a.files", "x-fern-sdk-method-name": "upload", + "paths": {"/a": {"post": {"x-forge-group-name": ["a", "files"], "x-forge-method-name": "upload", "requestBody": {"content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Upload"}}}}, "responses": {"200": {"description": "ok", "content": {"text/csv": {"schema": {"$ref": "#/components/schemas/Report"}}}}}}}}, "components": {"schemas": { @@ -287,9 +287,9 @@ fn use_by_content_that_is_not_json_places_a_model() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/b": {"post": {"x-fern-sdk-group-name": "b", "x-fern-sdk-method-name": "upload", + "/b": {"post": {"x-forge-group-name": ["b"], "x-forge-method-name": "upload", "requestBody": {"content": {"multipart/form-data": {"schema": { "type": "object", "properties": {"meta": {"$ref": "#/components/schemas/M"}} }}}}, @@ -307,7 +307,7 @@ fn schema_used_only_in_a_response_header_is_not_a_model() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "X-Trace", "in": "header", "schema": {"$ref": "#/components/schemas/Trace"}}], "responses": {"204": {"description": "ok", "headers": {"X-Rate-Limit": {"schema": {"$ref": "#/components/schemas/RateLimit"}}}}}}}}, diff --git a/tests/parse/requests.rs b/tests/parse/requests.rs index 31ad98776..b0e9eaaae 100644 --- a/tests/parse/requests.rs +++ b/tests/parse/requests.rs @@ -13,8 +13,8 @@ fn path_param_is_a_required_request_field() { "paths": { "/zones/{zone_id}": { "get": { - "x-fern-sdk-group-name": "zones", - "x-fern-sdk-method-name": "get", + "x-forge-group-name": ["zones"], + "x-forge-method-name": "get", "parameters": [ { "name": "zone_id", "in": "path", "required": true, "schema": { "type": "string" } } ], @@ -44,7 +44,7 @@ fn query_param_is_optional_and_exploded_by_default() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -61,7 +61,7 @@ fn query_param_can_be_required() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -76,7 +76,7 @@ fn query_param_with_explode_false_is_not_exploded() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "ids", "in": "query", "style": "form", "explode": false, "schema": {"type": "array", "items": {"type": "string"}}}], "responses": {"204": {"description": "ok"}}}}} @@ -93,7 +93,7 @@ fn header_and_cookie_params_are_not_fields() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "X-Trace", "in": "header", "schema": {"type": "string"}}, {"name": "session", "in": "cookie", "schema": {"type": "string"}} @@ -107,12 +107,12 @@ fn header_and_cookie_params_are_not_fields() { } #[test] -fn param_field_is_named_by_x_fern_parameter_name() { +fn param_field_is_named_by_x_forge_parameter_name() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", - "parameters": [{"name": "q", "in": "query", "x-fern-parameter-name": "query", "schema": {"type": "string"}}], + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", + "parameters": [{"name": "q", "in": "query", "x-forge-parameter-name": "query", "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -126,7 +126,7 @@ fn param_description_falls_back_to_an_inline_schema() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [ {"name": "own", "in": "query", "description": "Param.", "schema": {"type": "string", "description": "Schema."}}, {"name": "inherited", "in": "query", "schema": {"type": "string", "description": "Schema."}} @@ -145,7 +145,7 @@ fn param_description_comes_from_a_ref_schema() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [ {"name": "own", "in": "query", "description": "Param.", "schema": {"$ref": "#/components/schemas/Id"}}, {"name": "id", "in": "query", "schema": {"$ref": "#/components/schemas/Id"}} @@ -165,7 +165,7 @@ fn param_description_follows_a_chain_of_refs() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"$ref": "#/components/schemas/Id"}}], "responses": {"204": {"description": "ok"}}}}}, "components": {"schemas": { @@ -184,7 +184,7 @@ fn param_description_is_never_a_schema_title() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [ {"name": "id", "in": "query", "schema": {"$ref": "#/components/schemas/Id"}}, {"name": "inline", "in": "query", "schema": {"type": "string", "title": "Inline"}} @@ -204,7 +204,7 @@ fn description_next_to_a_param_schema_ref_is_its_own_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "id", "in": "query", "schema": {"$ref": "#/components/schemas/Id", "description": "The ID to find."}}], "responses": {"204": {"description": "ok"}}}}}, @@ -221,7 +221,7 @@ fn param_description_comes_from_an_all_of_member_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "region", "in": "query", "schema": {"allOf": [{"$ref": "#/components/schemas/Region"}, {"default": "us"}]}}], "responses": {"204": {"description": "ok"}}}}}, @@ -238,7 +238,7 @@ fn param_description_from_an_all_of_member_is_ignored_in_3_0() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "region", "in": "query", "schema": {"allOf": [{"$ref": "#/components/schemas/Region"}, {"default": "us"}]}}], "responses": {"204": {"description": "ok"}}}}}, @@ -255,7 +255,7 @@ fn param_ref_description_overrides_the_params_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"$ref": "#/components/parameters/Page", "description": "The page of pets."}], "responses": {"204": {"description": "ok"}}}}}, "components": {"parameters": {"Page": {"name": "page", "in": "query", "description": "A page.", @@ -272,7 +272,7 @@ fn param_ref_description_is_ignored_in_3_0() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"$ref": "#/components/parameters/Page", "description": "The page of pets."}], "responses": {"204": {"description": "ok"}}}}}, "components": {"parameters": {"Page": {"name": "page", "in": "query", "description": "A page.", @@ -289,7 +289,7 @@ fn param_ref_without_a_description_has_the_params() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"$ref": "#/components/parameters/Page"}], "responses": {"204": {"description": "ok"}}}}}, "components": {"parameters": {"Page": {"name": "page", "in": "query", "description": "A page.", @@ -306,7 +306,7 @@ fn param_field_is_never_read_only() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "id", "in": "path", "required": true, "schema": {"type": "string", "readOnly": true}}, {"name": "since", "in": "query", "schema": {"$ref": "#/components/schemas/Since", "readOnly": true}}, @@ -331,9 +331,9 @@ fn query_string_property_is_never_read_only() { let forge = parse( r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "filter", "in": "querystring", "content": {"application/x-www-form-urlencoded": {"schema": { - "type": "object", "properties": {"status": {"type": "string", "readOnly": true, "x-sensitive": true}} + "type": "object", "properties": {"status": {"type": "string", "readOnly": true, "x-forge-sensitive": true}} }}}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -349,9 +349,9 @@ fn param_field_is_sensitive_when_its_schema_is() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [ - {"name": "token", "in": "query", "schema": {"type": "string", "x-sensitive": true}}, + {"name": "token", "in": "query", "schema": {"type": "string", "x-forge-sensitive": true}}, {"name": "page", "in": "query", "schema": {"type": "integer"}} ], "responses": {"204": {"description": "ok"}}}}} @@ -368,12 +368,12 @@ fn param_field_is_sensitive_when_its_ref_schema_is() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "token", "in": "query", "schema": {"$ref": "#/components/schemas/Token"}}], "responses": {"204": {"description": "ok"}}}}}, "components": {"schemas": { "Token": {"$ref": "#/components/schemas/Secret"}, - "Secret": {"type": "string", "x-sensitive": true} + "Secret": {"type": "string", "x-forge-sensitive": true} }} }"##, ); @@ -386,9 +386,9 @@ fn sensitive_next_to_a_param_schema_ref_counts_in_3_0() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "token", "in": "query", - "schema": {"$ref": "#/components/schemas/Token", "x-sensitive": true}}], + "schema": {"$ref": "#/components/schemas/Token", "x-forge-sensitive": true}}], "responses": {"204": {"description": "ok"}}}}}, "components": {"schemas": {"Token": {"type": "string"}}} }"##, @@ -402,8 +402,8 @@ fn sensitive_on_the_param_itself_is_ignored() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", - "parameters": [{"name": "token", "in": "query", "x-sensitive": true, "schema": {"type": "string"}}], + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", + "parameters": [{"name": "token", "in": "query", "x-forge-sensitive": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -416,7 +416,7 @@ fn param_ref_is_followed() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"$ref": "#/components/parameters/Page"}], "responses": {"204": {"description": "ok"}}}}}, "components": {"parameters": {"Page": {"name": "page", "in": "query", "schema": {"type": "integer"}}}} @@ -435,8 +435,8 @@ fn path_item_params_belong_to_each_operation() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a/{id}": { "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", "responses": {"204": {"description": "ok"}}}, - "delete": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "delete", "responses": {"204": {"description": "ok"}}} + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-group-name": ["a"], "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}} }} }"#, ); @@ -455,7 +455,7 @@ fn operation_param_replaces_the_path_item_param() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { "parameters": [{"name": "page", "in": "query", "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}} }} @@ -472,7 +472,7 @@ fn undeclared_path_placeholder_is_a_required_string() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -491,7 +491,7 @@ fn params_are_in_path_template_then_declared_order() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a/{second}/{first}": { "parameters": [{"name": "item_query", "in": "query", "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "op_query", "in": "query", "schema": {"type": "string"}}, {"name": "first", "in": "path", "required": true, "schema": {"type": "string"}}, @@ -511,7 +511,7 @@ fn query_string_object_is_query_fields() { let forge = parse( r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "filter", "in": "querystring", "content": {"application/x-www-form-urlencoded": {"schema": { "type": "object", "properties": {"status": {"type": "string"}, "limit": {"type": "integer"}} }}}}], @@ -532,7 +532,7 @@ fn no_params_and_no_body_is_no_request() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -545,7 +545,7 @@ fn body_without_params_is_the_request() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -559,7 +559,7 @@ fn object_body_is_intersected_with_the_params() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": { "type": "object", "properties": {"name": {"type": "string"}} @@ -582,7 +582,7 @@ fn model_body_is_intersected_with_the_params() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}}, "responses": {"204": {"description": "ok"}}}}}, @@ -604,7 +604,7 @@ fn union_body_is_intersected_with_the_params() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": { "oneOf": [{"$ref": "#/components/schemas/Cat"}, {"$ref": "#/components/schemas/Dog"}] @@ -631,7 +631,7 @@ fn array_body_is_the_last_field_of_the_params() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "responses": {"204": {"description": "ok"}}}}} @@ -651,10 +651,10 @@ fn body_field_is_neither_read_only_nor_sensitive() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": { - "type": "array", "items": {"type": "string"}, "readOnly": true, "x-sensitive": true + "type": "array", "items": {"type": "string"}, "readOnly": true, "x-forge-sensitive": true }}}}, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -670,7 +670,7 @@ fn read_only_field_of_a_body_model_is_kept() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}}, "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}}}}}}, "components": {"schemas": {"Pet": {"type": "object", "properties": { @@ -690,7 +690,7 @@ fn map_body_is_a_body_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": { "type": "object", "additionalProperties": {"type": "string"} @@ -708,7 +708,7 @@ fn unknown_body_is_a_body_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {}}}}, "responses": {"204": {"description": "ok"}}}}} @@ -724,7 +724,7 @@ fn nullable_body_is_a_body_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": { "type": "object", "nullable": true, "properties": {"name": {"type": "string"}} @@ -742,7 +742,7 @@ fn body_with_the_name_of_a_param_is_a_body_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{zone_id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{zone_id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "zone_id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": { "type": "object", "properties": {"zoneId": {"type": "string"}, "name": {"type": "string"}} @@ -762,8 +762,8 @@ fn field_of_another_method_has_no_location() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a/{id}": { "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", "responses": {"204": {"description": "ok"}}}, - "delete": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "delete", "responses": {"204": {"description": "ok"}}} + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-group-name": ["a"], "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}} }} }"#, ); @@ -779,7 +779,7 @@ fn body_that_is_not_json_is_an_unknown_body_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "upload", + "paths": {"/a/{id}": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "upload", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"multipart/form-data": {"schema": { "type": "object", "properties": {"file": {"type": "string", "format": "binary"}} @@ -798,7 +798,7 @@ fn body_that_is_only_multipart_is_an_unknown_request() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "upload", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "upload", "requestBody": {"content": {"multipart/form-data": {"schema": { "type": "object", "properties": {"file": {"type": "string", "format": "binary"}} }}}}, @@ -816,7 +816,7 @@ fn path_param_missing_from_the_template_is_dropped() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "account_id", "in": "path", "required": true, "schema": {"type": "string"}} @@ -833,7 +833,7 @@ fn deprecated_query_param_is_a_deprecated_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "deprecated": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -847,7 +847,7 @@ fn deprecated_path_param_is_not_a_deprecated_field() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{"name": "id", "in": "path", "required": true, "deprecated": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -861,7 +861,7 @@ fn all_of_body_is_one_member_of_the_intersection() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": { "allOf": [{"$ref": "#/components/schemas/A"}, {"$ref": "#/components/schemas/B"}] @@ -888,7 +888,7 @@ fn param_with_content_is_an_error() { let err = parse_err( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "filter", "in": "query", "content": {"application/json": {"schema": { "type": "object", "properties": {"status": {"type": "string"}} }}}}], @@ -912,7 +912,7 @@ fn query_style_other_than_form_is_an_error() { let err = parse_err(&format!( r#"{{ "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, - "paths": {{"/a": {{"get": {{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {{"/a": {{"get": {{"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{{"name": "filter", "in": "query", "style": "{style}", "schema": {schema}}}], "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# @@ -927,7 +927,7 @@ fn two_query_string_params_are_an_error() { let err = parse_err( r#"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [ {"name": "one", "in": "querystring", "content": {"application/x-www-form-urlencoded": {"schema": {"type": "object"}}}}, {"name": "two", "in": "querystring", "content": {"application/x-www-form-urlencoded": {"schema": {"type": "object"}}}} @@ -946,7 +946,7 @@ fn query_and_query_string_params_are_an_error() { "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, "paths": {"/a": { "parameters": [{"name": "page", "in": "query", "schema": {"type": "integer"}}], - "get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "filter", "in": "querystring", "content": {"application/x-www-form-urlencoded": {"schema": {"type": "object"}}}}], "responses": {"204": {"description": "ok"}}} }} @@ -963,10 +963,10 @@ fn body_field_is_required_when_the_body_is() { "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a/{id}": { "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], - "put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "replace", + "put": {"x-forge-group-name": ["a"], "x-forge-method-name": "replace", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "responses": {"204": {"description": "ok"}}}, - "patch": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "append", + "patch": {"x-forge-group-name": ["a"], "x-forge-method-name": "append", "requestBody": {"content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "responses": {"204": {"description": "ok"}}} }} @@ -983,7 +983,7 @@ fn params_with_equal_names_are_an_error() { let err = parse_err( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{zone_id}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a/{zone_id}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [ {"name": "zone_id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "zoneId", "in": "query", "schema": {"type": "string"}} @@ -1001,7 +1001,7 @@ fn path_style_other_than_simple_is_an_error() { let err = parse_err(&format!( r#"{{ "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, - "paths": {{"/a/{{id}}": {{"get": {{"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {{"/a/{{id}}": {{"get": {{"x-forge-group-name": ["a"], "x-forge-method-name": "get", "parameters": [{{"name": "id", "in": "path", "required": true, "style": "{style}", "schema": {{"type": "string"}}}}], "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# @@ -1016,7 +1016,7 @@ fn param_field_has_the_default_of_its_schema() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [ {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}}, {"name": "page", "in": "query", "schema": {"type": "integer"}} @@ -1035,7 +1035,7 @@ fn param_field_has_the_default_of_its_ref_schema() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{order}": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a/{order}": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [ {"name": "order", "in": "path", "required": true, "schema": {"$ref": "#/components/schemas/Order"}}, {"$ref": "#/components/parameters/Direction"} @@ -1062,7 +1062,7 @@ fn default_next_to_a_param_schema_ref_is_its_own_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "per_page", "in": "query", "schema": {"$ref": "#/components/schemas/PerPage", "default": 50}}], "responses": {"204": {"description": "ok"}}}}}, @@ -1079,7 +1079,7 @@ fn default_on_the_param_itself_is_ignored() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "list", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "list", "parameters": [{"name": "per_page", "in": "query", "default": 20, "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -1096,7 +1096,7 @@ fn body_field_and_undeclared_placeholder_have_no_default() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a/{id}": {"put": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-group-name": ["a"], "x-forge-method-name": "update", "requestBody": {"content": {"application/json": {"schema": { "type": "array", "items": {"type": "string"}, "default": ["x"] }}}}, @@ -1114,7 +1114,7 @@ fn array_body_keeps_its_max_items() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "requestBody": {"content": {"application/json": {"schema": { "type": "array", "items": {"type": "string"}, "maxItems": 100 }}}}, diff --git a/tests/parse/responses.rs b/tests/parse/responses.rs index a34167acc..6555d6935 100644 --- a/tests/parse/responses.rs +++ b/tests/parse/responses.rs @@ -7,7 +7,7 @@ fn responses_are_the_2xx_sorted_by_status() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"post": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "create", + "paths": {"/a": {"post": {"x-forge-group-name": ["a"], "x-forge-method-name": "create", "responses": { "2XX": {"description": "ok"}, "201": {"description": "ok"}, @@ -29,7 +29,7 @@ fn errors_are_the_rest_sorted_with_default_last() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": { "default": {"description": "no"}, "500": {"description": "no"}, @@ -54,7 +54,7 @@ fn response_has_its_description() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "The pet."}}}}} }"#, ); @@ -68,7 +68,7 @@ fn response_without_content_has_none() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"delete": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "delete", + "paths": {"/a": {"delete": {"x-forge-group-name": ["a"], "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -83,7 +83,7 @@ fn response_ref_is_followed() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"4XX": {"$ref": "#/components/responses/Failure"}}}}}, "components": {"responses": { "Failure": {"description": "Failed.", "content": {"application/json": {"schema": {"type": "string"}}}} @@ -102,7 +102,7 @@ fn response_content_that_is_not_json_is_typed() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "download", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "download", "responses": {"200": {"description": "ok", "content": { "application/octet-stream": {"schema": {"type": "string", "format": "binary"}}, "text/csv": {"schema": {"type": "string"}} diff --git a/tests/parse/types/arrays.rs b/tests/parse/types/arrays.rs index 262c601e1..88caa3184 100644 --- a/tests/parse/types/arrays.rs +++ b/tests/parse/types/arrays.rs @@ -92,7 +92,7 @@ fn array_without_items_has_unknown_elements() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "array" }}}}}}}} @@ -107,7 +107,7 @@ fn tuple_is_unknown() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "array", "prefixItems": [{"type": "string"}, {"type": "integer"}], "items": false }}}}}}}} diff --git a/tests/parse/types/enums.rs b/tests/parse/types/enums.rs index d2b3b497e..1e331ab35 100644 --- a/tests/parse/types/enums.rs +++ b/tests/parse/types/enums.rs @@ -59,7 +59,7 @@ fn const_is_an_enum_of_one_value() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "const": "a" }}}}}}}} diff --git a/tests/parse/types/intersections.rs b/tests/parse/types/intersections.rs index 7c5d6564b..0794b2739 100644 --- a/tests/parse/types/intersections.rs +++ b/tests/parse/types/intersections.rs @@ -49,7 +49,7 @@ fn true_member_is_dropped() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "allOf": [{"$ref": "#/components/schemas/A"}, true, {"$ref": "#/components/schemas/B"}] }}}}}}}}, @@ -141,7 +141,7 @@ fn unevaluated_properties_is_an_object_member() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "allOf": [{"$ref": "#/components/schemas/A"}], "unevaluatedProperties": {"type": "string"} diff --git a/tests/parse/types/nulls.rs b/tests/parse/types/nulls.rs index a3794823e..79ee061b7 100644 --- a/tests/parse/types/nulls.rs +++ b/tests/parse/types/nulls.rs @@ -95,7 +95,7 @@ fn nullable_is_read_in_3_1() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "string", "nullable": true }}}}}}}} @@ -110,7 +110,7 @@ fn null_in_a_type_array_is_a_union_with_null() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": ["string", "null"] }}}}}}}} @@ -125,7 +125,7 @@ fn null_type_is_null() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "null" }}}}}}}} @@ -140,7 +140,7 @@ fn const_null_is_null() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "const": null }}}}}}}} diff --git a/tests/parse/types/objects.rs b/tests/parse/types/objects.rs index 1b5139d46..7ade16797 100644 --- a/tests/parse/types/objects.rs +++ b/tests/parse/types/objects.rs @@ -36,9 +36,9 @@ fn required_names_without_a_property_are_ignored() { } #[test] -fn field_is_named_by_x_fern_property_name() { +fn field_is_named_by_x_forge_property_name() { let forge = parse(&responding_with( - r#"{"type": "object", "properties": {"owner": {"type": "string", "x-fern-property-name": "person"}}}"#, + r#"{"type": "object", "properties": {"owner": {"type": "string", "x-forge-property-name": "person"}}}"#, )); let owner = field(object(response_type(&forge, "a.get")), "owner"); @@ -78,7 +78,7 @@ fn field_is_read_only_when_its_property_is() { #[test] fn field_is_neither_read_only_nor_sensitive_by_default() { let forge = parse(&responding_with( - r#"{"type": "object", "properties": {"name": {"type": "string"}, "shown": {"type": "string", "readOnly": false, "x-sensitive": false}}}"#, + r#"{"type": "object", "properties": {"name": {"type": "string"}, "shown": {"type": "string", "readOnly": false, "x-forge-sensitive": false}}}"#, )); let object = object(response_type(&forge, "a.get")); @@ -100,7 +100,7 @@ fn read_only_that_is_not_a_boolean_is_an_error() { #[test] fn field_is_sensitive_when_its_property_is() { let forge = parse(&responding_with( - r#"{"type": "object", "properties": {"password": {"type": "string", "x-sensitive": true}}}"#, + r#"{"type": "object", "properties": {"password": {"type": "string", "x-forge-sensitive": true}}}"#, )); assert!(field(object(response_type(&forge, "a.get")), "password").is_sensitive()); @@ -109,7 +109,7 @@ fn field_is_sensitive_when_its_property_is() { #[test] fn sensitive_that_is_not_a_boolean_is_an_error() { let err = parse_err(&responding_with( - r#"{"type": "object", "properties": {"password": {"type": "string", "x-sensitive": "true"}}}"#, + r#"{"type": "object", "properties": {"password": {"type": "string", "x-forge-sensitive": "true"}}}"#, )); assert!(err.contains("expected a boolean"), "{err}"); @@ -195,7 +195,7 @@ fn pattern_properties_are_ignored() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"id": {"type": "string"}}, "patternProperties": {"^x-": {"type": "string"}} }}}}}}}} @@ -212,7 +212,7 @@ fn false_property_is_no_field() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"id": {"type": "string"}, "gone": false} }}}}}}}} @@ -223,9 +223,9 @@ fn false_property_is_no_field() { } #[test] -fn property_skipped_by_x_stainless_skip_is_no_field() { +fn property_skipped_by_x_forge_skip_is_no_field() { let forge = parse(&responding_with( - r#"{"type": "object", "properties": {"id": {"type": "string"}, "old_id": {"type": "string", "x-stainless-skip": true}}}"#, + r#"{"type": "object", "properties": {"id": {"type": "string"}, "old_id": {"type": "string", "x-forge-skip": true}}}"#, )); assert_eq!(field_names(object(response_type(&forge, "a.get"))), ["id"]); @@ -297,3 +297,52 @@ fn default_is_not_checked_against_the_schema() { let limit = field(object(response_type(&forge, "a.get")), "limit"); assert_eq!(limit.default(), Some(&Json::String("ten".into()))); } + +#[test] +fn field_is_sensitive_by_the_deprecated_x_sensitive() { + let forge = parse(&responding_with( + r#"{"type": "object", "properties": {"password": {"type": "string", "x-sensitive": true}}}"#, + )); + + assert!(field(object(response_type(&forge, "a.get")), "password").is_sensitive()); +} + +#[test] +fn x_forge_sensitive_wins_over_the_deprecated_x_sensitive_in_either_order() { + for (keys, sensitive) in [ + (r#""x-forge-sensitive": false, "x-sensitive": true"#, false), + (r#""x-sensitive": true, "x-forge-sensitive": false"#, false), + (r#""x-forge-sensitive": true, "x-sensitive": false"#, true), + (r#""x-sensitive": false, "x-forge-sensitive": true"#, true), + ] { + let forge = parse(&responding_with(&format!( + r#"{{"type": "object", "properties": {{"password": {{"type": "string", {keys}}}}}}}"# + ))); + + assert_eq!( + field(object(response_type(&forge, "a.get")), "password").is_sensitive(), + sensitive, + "{keys}" + ); + } +} + +#[test] +fn deprecated_x_sensitive_that_is_not_a_boolean_is_an_error_even_beside_x_forge_sensitive() { + let err = parse_err(&responding_with( + r#"{"type": "object", "properties": {"password": {"type": "string", "x-forge-sensitive": true, "x-sensitive": "yes"}}}"#, + )); + + assert!(err.contains("invalid type"), "{err}"); +} + +#[test] +fn sensitive_written_twice_under_one_name_is_an_error_naming_it() { + for key in ["x-forge-sensitive", "x-sensitive"] { + let err = parse_err(&responding_with(&format!( + r#"{{"type": "object", "properties": {{"password": {{"type": "string", "{key}": true, "{key}": true}}}}}}"# + ))); + + assert!(err.contains(&format!("duplicate field `{key}`")), "{key}: {err}"); + } +} diff --git a/tests/parse/types/refs.rs b/tests/parse/types/refs.rs index 0e39e86e3..6c6429a6d 100644 --- a/tests/parse/types/refs.rs +++ b/tests/parse/types/refs.rs @@ -65,7 +65,7 @@ fn annotations_next_to_a_ref_keep_the_model_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "$ref": "#/components/schemas/Zone", "description": "The zone.", "deprecated": true }}}}}}}}, @@ -81,7 +81,7 @@ fn shape_next_to_a_ref_is_intersected_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "$ref": "#/components/schemas/Zone", "properties": {"extra": {"type": "string"}} }}}}}}}}, @@ -97,7 +97,7 @@ fn nullable_next_to_a_ref_is_a_union_with_null_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "$ref": "#/components/schemas/Zone", "nullable": true }}}}}}}}, @@ -153,7 +153,7 @@ fn description_next_to_a_property_ref_is_its_own_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"home": {"$ref": "#/components/schemas/Zone", "description": "The home zone."}} }}}}}}}}, @@ -324,7 +324,7 @@ fn read_only_next_to_a_property_ref_counts_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"id": {"$ref": "#/components/schemas/Id", "readOnly": true}} }}}}}}}}, @@ -350,7 +350,7 @@ fn read_only_false_next_to_a_ref_does_not_undo_its_schemas_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"id": {"$ref": "#/components/schemas/Id", "readOnly": false}} }}}}}}}}, @@ -365,7 +365,7 @@ fn read_only_false_next_to_a_ref_does_not_undo_its_schemas_in_3_1() { fn property_that_is_a_ref_is_sensitive_when_its_schema_is() { let forge = parse(&responding_with_schemas( r##"{"type": "object", "properties": {"token": {"$ref": "#/components/schemas/Token"}}}"##, - r##"{"Token": {"$ref": "#/components/schemas/Secret"}, "Secret": {"type": "string", "x-sensitive": true}}"##, + r##"{"Token": {"$ref": "#/components/schemas/Secret"}, "Secret": {"type": "string", "x-forge-sensitive": true}}"##, )); assert!(field(object(response_type(&forge, "a.get")), "token").is_sensitive()); @@ -374,7 +374,7 @@ fn property_that_is_a_ref_is_sensitive_when_its_schema_is() { #[test] fn sensitive_next_to_a_property_ref_counts_in_3_0() { let forge = parse(&responding_with_schemas( - r##"{"type": "object", "properties": {"token": {"$ref": "#/components/schemas/Token", "x-sensitive": true}}}"##, + r##"{"type": "object", "properties": {"token": {"$ref": "#/components/schemas/Token", "x-forge-sensitive": true}}}"##, r#"{"Token": {"type": "string"}}"#, )); @@ -386,9 +386,9 @@ fn sensitive_next_to_a_property_ref_counts_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { - "type": "object", "properties": {"token": {"$ref": "#/components/schemas/Token", "x-sensitive": true}} + "type": "object", "properties": {"token": {"$ref": "#/components/schemas/Token", "x-forge-sensitive": true}} }}}}}}}}, "components": {"schemas": {"Token": {"type": "string"}}} }"##, @@ -424,7 +424,7 @@ fn property_default_is_the_first_along_a_chain_of_refs_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"size": {"$ref": "#/components/schemas/Size"}} }}}}}}}}, @@ -454,7 +454,7 @@ fn default_next_to_a_property_ref_is_its_own_in_3_1() { let forge = parse( r##"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": { "size": {"$ref": "#/components/schemas/Size", "default": 5}, diff --git a/tests/parse/types/scalars.rs b/tests/parse/types/scalars.rs index 810e9954a..3ec41e305 100644 --- a/tests/parse/types/scalars.rs +++ b/tests/parse/types/scalars.rs @@ -140,7 +140,7 @@ fn base64_content_encoding_is_bytes() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "string", "contentEncoding": "base64" }}}}}}}} @@ -155,7 +155,7 @@ fn binary_format_is_binary_even_with_base64_content_encoding() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "string", "format": "binary", "contentEncoding": "base64" }}}}}}}} @@ -198,7 +198,7 @@ fn true_schema_is_unknown() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": true}}}}}}} }"#, ); diff --git a/tests/parse/types/unions.rs b/tests/parse/types/unions.rs index 45f11f81d..8f26b6e38 100644 --- a/tests/parse/types/unions.rs +++ b/tests/parse/types/unions.rs @@ -76,7 +76,7 @@ fn type_array_is_exclusive() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": ["string", "integer"] }}}}}}}} @@ -138,7 +138,7 @@ fn types_in_a_type_array_are_a_union() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": ["string", "integer"] }}}}}}}} @@ -291,15 +291,15 @@ fn discriminator_of_members_written_in_place_uses_the_one_value_of_an_enum() { } #[test] -fn discriminator_is_named_by_x_fern_property_name() { +fn discriminator_is_named_by_x_forge_property_name() { let forge = parse(&responding_with_schemas( r##"{ "oneOf": [{"$ref": "#/components/schemas/A"}, {"$ref": "#/components/schemas/B"}], "discriminator": {"propertyName": "kind"} }"##, r#"{ - "A": {"type": "object", "properties": {"kind": {"type": "string", "x-fern-property-name": "pet_kind"}}}, - "B": {"type": "object", "properties": {"kind": {"type": "string", "x-fern-property-name": "pet_kind"}}} + "A": {"type": "object", "properties": {"kind": {"type": "string", "x-forge-property-name": "pet_kind"}}}, + "B": {"type": "object", "properties": {"kind": {"type": "string", "x-forge-property-name": "pet_kind"}}} }"#, )); @@ -329,7 +329,7 @@ fn discriminator_default_mapping_is_the_default() { let forge = parse( r##"{ "openapi": "3.2.0", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-fern-sdk-group-name": "a", "x-fern-sdk-method-name": "get", + "paths": {"/a": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "oneOf": [{"$ref": "#/components/schemas/A"}, {"$ref": "#/components/schemas/B"}], "discriminator": {"propertyName": "kind", "mapping": {"a": "#/components/schemas/A"}, "defaultMapping": "#/components/schemas/B"} diff --git a/tests/wasm.rs b/tests/wasm.rs index 7278ead1d..18bedd8b9 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -41,7 +41,7 @@ const SPEC: &str = r##"{ "paths": { "/pets": { "get": { - "x-fern-sdk-group-name": "pets", "x-fern-sdk-method-name": "list", "summary": "Lists pets.", + "x-forge-group-name": ["pets"], "x-forge-method-name": "list", "summary": "Lists pets.", "parameters": [ {"name": "limit", "in": "query", "schema": {"type": "integer", "format": "int32"}}, {"name": "tags", "in": "query", "explode": false, "schema": {"type": "array", "items": {"type": "string"}}} @@ -52,14 +52,14 @@ const SPEC: &str = r##"{ } }, "post": { - "x-fern-sdk-group-name": "pets", "x-fern-sdk-method-name": "create", "deprecated": true, + "x-forge-group-name": ["pets"], "x-forge-method-name": "create", "deprecated": true, "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/NewPet"}}}}, "responses": {"201": {"description": "created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}}} } }, "/pets/{pet_id}": { "get": { - "x-fern-sdk-group-name": "pets", "x-fern-sdk-method-name": "get", + "x-forge-group-name": ["pets"], "x-forge-method-name": "get", "parameters": [{"name": "pet_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": { "200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}}, @@ -69,7 +69,7 @@ const SPEC: &str = r##"{ }, "/pets/{pet_id}/photos": { "put": { - "x-fern-sdk-group-name": "pets.photos", "x-fern-sdk-method-name": "upload", + "x-forge-group-name": ["pets", "photos"], "x-forge-method-name": "upload", "parameters": [{"name": "pet_id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}}, "responses": {"204": {"description": "done"}} @@ -77,7 +77,7 @@ const SPEC: &str = r##"{ }, "/owners": { "get": { - "x-fern-sdk-group-name": "owners", "x-fern-sdk-method-name": "list", + "x-forge-group-name": ["owners"], "x-forge-method-name": "list", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": { @@ -138,23 +138,23 @@ const FEATURES: &str = r##"{ "/pets/{id}": { "parameters": [{"name": "X-Trace", "in": "header", "schema": {"type": "string"}}], "get": { - "x-fern-sdk-group-name": "pets", "x-fern-sdk-method-name": "get", - "x-fern-availability": "beta", "x-forge-hidden": true, + "x-forge-group-name": ["pets"], "x-forge-method-name": "get", + "x-forge-availability": "beta", "x-forge-hidden": true, "x-custom": {"b": 1, "a": [1.5, "two \"quoted\"\n", null, true, {}], "b": 2}, "x-flag": false, "x-count": 9007199254740993, "x-text": "é 😀", "parameters": [ {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 10}}, {"name": "x-trace", "in": "header", "required": true, "description": "Traces the call.", "schema": {"type": "string"}}, - {"name": "Authorization", "in": "header", "schema": {"type": "string", "x-sensitive": true}} + {"name": "Authorization", "in": "header", "schema": {"type": "string", "x-forge-sensitive": true}} ], "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}}} }, "delete": { "x-forge-aliases": [ - {"x-fern-sdk-group-name": "pets", "x-fern-sdk-method-name": "remove", "x-fern-availability": "deprecated", + {"x-forge-group-name": ["pets"], "x-forge-method-name": "remove", "x-forge-availability": "deprecated", "x-forge-require-confirmation": "This deletes the pet.", "x-alias": [1]}, - {"x-fern-sdk-group-name": "admin.pets", "x-fern-sdk-method-name": "delete", "x-forge-require-confirmation": ""} + {"x-forge-group-name": ["admin", "pets"], "x-forge-method-name": "delete", "x-forge-require-confirmation": ""} ], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "gone"}} @@ -162,7 +162,7 @@ const FEATURES: &str = r##"{ }, "/pets/{id}/photo": { "put": { - "x-fern-sdk-group-name": "pets.photos", "x-fern-sdk-method-name": "upload", + "x-forge-group-name": ["pets", "photos"], "x-forge-method-name": "upload", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"required": true, "content": { "application/octet-stream": {"schema": {"type": "string", "format": "binary", "description": "The photo."}}, @@ -178,7 +178,7 @@ const FEATURES: &str = r##"{ "components": {"schemas": { "Pet": {"type": "object", "properties": { "id": {"type": "string", "readOnly": true}, - "password": {"type": "string", "x-sensitive": true}, + "password": {"type": "string", "x-forge-sensitive": true}, "tags": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 3}, "dataset_scope": {"oneOf": [ {"type": "object", "properties": {"type": {"const": "default"}}, "required": ["type"], "additionalProperties": false}, diff --git a/tests/wasm/ts/wit/forge.wit b/tests/wasm/ts/wit/forge.wit index af93aa10e..7a489f74e 100644 --- a/tests/wasm/ts/wit/forge.wit +++ b/tests/wasm/ts/wit/forge.wit @@ -82,7 +82,7 @@ interface forge { json: option, } - /// The stage of the API a method is in, from `x-fern-availability`. + /// The stage of the API a method is in, from `x-forge-availability`. enum availability { alpha, beta, preview, generally-available, deprecated, }