diff --git a/conformance/RFC.md b/conformance/RFC.md index dc726120c..b47296e5e 100644 --- a/conformance/RFC.md +++ b/conformance/RFC.md @@ -44,17 +44,17 @@ start, and knows nothing about the target's language or toolchain. One OpenAPI document, written for the cases, with a fixed set of 9 methods: -| Method | Route | Covers | -| -------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `health` | `GET /health` | root method, bearer `token` header, no parameters | -| `projects.create` | `POST /orgs/{org_id}/projects` | SDK variable (`org_id`), object body, `date-time`, `date`, `int64`, `byte`, 201 | -| `projects.get` | `GET /orgs/{org_id}/projects/{project_id}` | path parameters, declared errors (404, `4XX`), undeclared status, unknown response properties | -| `projects.update` | `PATCH /orgs/{org_id}/projects/{project_id}` | path, query (repeated and special-character names) and header parameters with an object body, body property colliding with a parameter name | -| `projects.delete` | `DELETE /orgs/{org_id}/projects/{project_id}` | 204 response with no body | -| `projects.archive` | `POST /orgs/{org_id}/projects/{project_id}/archive` | method and subgroup with the same name | -| `projects.archive.restore` | `POST /orgs/{org_id}/projects/{project_id}/archive/restore` | subgroup on a method | -| `projects.members.replace` | `PUT /orgs/{org_id}/projects/{project_id}/members` | nested group, array body, two success statuses (200 and 201) with different bodies | -| `dnsRecords.getById` | `POST /zones/{zone_id}/dns-records/{record_id}` | kebab-case group (`dns-records`), camelCase method with acronym (`getByID`), primitives, enums, maps, `nullable`, `allOf`, `oneOf`, `anyOf`, recursive models | +| Method | Route | Covers | +| -------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `health` | `GET /health` | root method, bearer `token` header, no parameters | +| `projects.create` | `POST /orgs/{org_id}/projects` | SDK variable (`org_id`), object body, `date-time`, `date`, `int64`, `byte`, 201 | +| `projects.get` | `GET /orgs/{org_id}/projects/{project_id}` | path parameters, declared errors (404, `4XX`), undeclared status, unknown response properties | +| `projects.update` | `PATCH /orgs/{org_id}/projects/{project_id}` | path, query (repeated and special-character names) and header parameters with an object body, body property colliding with a parameter name | +| `projects.delete` | `DELETE /orgs/{org_id}/projects/{project_id}` | 204 response with no body | +| `projects.archive` | `POST /orgs/{org_id}/projects/{project_id}/archive` | method and subnamespace with the same name | +| `projects.archive.restore` | `POST /orgs/{org_id}/projects/{project_id}/archive/restore` | subnamespace on a method | +| `projects.members.replace` | `PUT /orgs/{org_id}/projects/{project_id}/members` | nested namespace, array body, two success statuses (200 and 201) with different bodies | +| `dnsRecords.getById` | `POST /zones/{zone_id}/dns-records/{record_id}` | kebab-case namespace (`dns-records`), camelCase method with acronym (`getByID`), primitives, enums, maps, `nullable`, `allOf`, `oneOf`, `anyOf`, recursive models | Every method in the suite API has at least one case. @@ -67,7 +67,7 @@ the caller must get. Cases are YAML files, each a list of cases. - name: an object body is sent in wire names variables: { orgId: o1 } call: - group: projects + namespace: projects method: create body: { displayName: Demo } exchange: @@ -91,8 +91,8 @@ type Case = { name: string; // unique across all case files variables?: Record; // SDK variables (x-fern-sdk-variable), set when the client is created call: { - group: string; // property path from the root client, e.g. "projects.members"; "" for root - method: string; // method name on that group + namespace: string; // property path from the root client, e.g. "projects.members"; "" for root + method: string; // method name on that namespace path?: Record; // path parameters, by field name query?: Record; // query parameters, by field name headers?: Record; // header parameters, by field name @@ -190,7 +190,7 @@ Input, the case's client side plus where to send requests: "baseUrl": "http://127.0.0.1:52100/", "token": "conformance-token", "variables": { "orgId": "o1" }, - "call": { "group": "projects", "method": "create", "body": { "displayName": "Demo" } } + "call": { "namespace": "projects", "method": "create", "body": { "displayName": "Demo" } } } ``` @@ -205,7 +205,7 @@ Output, with the same id and a `Result`, or `unexpected`: For each line the program: 1. creates a client from `baseUrl`, `token` and `variables`; -2. calls `call.method` on `call.group` with the inputs in `call`; +2. calls `call.method` on `call.namespace` with the inputs in `call`; 3. maps the return value or error to a `Result`, converting tagged values to and from the language's types. diff --git a/packages/forge/README.md b/packages/forge/README.md index 75d6b62a9..ec03259ee 100644 --- a/packages/forge/README.md +++ b/packages/forge/README.md @@ -55,7 +55,7 @@ to select more than one. For example, What is rewritten: -- A group path written as an array becomes the names joined by dots +- A namespace path written as an array becomes the names joined by dots (`["a", "b"]` becomes `"a.b"`), unless a name has a dot in it. - A skip for only some targets, written as a list, has no native form, since `x-forge-skip` is for every target: it is left as it is. A skip that is plainly @@ -88,7 +88,7 @@ has a key to rewrite is not a minimal edit. The order of its keys is kept. ## Plugins A plugin generates an SDK, or anything else, from the document: forge parses -it, and hands the plugin its groups, methods, models and types. Plugins are +it, and hands the plugin its namespaces, methods, models and types. Plugins are WebAssembly components, so they run anywhere forge does, sandboxed: a plugin reads nothing but the document, and its only output is the files it returns. @@ -120,16 +120,16 @@ function ts(type: Type): string { } } -export const { generate, generateGroup } = definePlugin({ +export const { generate, generateNamespace } = definePlugin({ // Called once, for the whole document. generate(forge) { return [{ path: 'README.md', content: `# API ${forge.version}\n` }]; }, - // Called once for each group, in parallel. - generateGroup(group) { - const dir = group.path.map((name) => name.camel()).join('/'); - const lines = group.models.map((m) => `export type ${m.name.pascal()} = ${ts(m.type)};`); + // Called once for each namespace, in parallel. + generateNamespace(namespace) { + const dir = namespace.path.map((name) => name.camel()).join('/'); + const lines = namespace.models.map((m) => `export type ${m.name.pascal()} = ${ts(m.type)};`); return [{ path: `${dir}/models.ts`, content: lines.join('\n') }]; }, }); @@ -144,7 +144,7 @@ npx forge openapi.json --target wasm://plugin.wasm --out sdk `@cloudflare/forge/plugin` is typed throughout. A type is a union you narrow by `kind`, so a `never` check in a `switch` turns a kind you missed into a -compile error. The same group, method or model is always the same object, +compile error. The same namespace, method or model is always the same object, so you can compare them with `===` and keep them in a `Map`. Array types expose `minItems` and `maxItems`; an absent bound is @@ -153,9 +153,14 @@ Array types expose `minItems` and `maxItems`; an absent bound is can handle it explicitly. Other unrecognized string formats remain `scalar: 'string'`. Forge does not validate request values. -The plugin interface is `forge:plugin@0.3.0`. Rebuild plugins compiled -against `0.2.0` with the updated package before running them with this -version of Forge. +The plugin interface is `forge:plugin@0.4.0`. It renames groups to namespaces, +so rebuild plugins compiled against `0.3.0` with the updated package before +running them with this version of Forge. The WIT intentionally has no group +aliases. Rust and TypeScript source APIs keep deprecated `Group`, `groups`, +`group` and `generateGroup` aliases where possible; OpenAPI input keeps +`x-forge-group-name` and nested `groups` in `x-forge-commands` as deprecated +aliases. New code and documents should use `Namespace`, `namespaces`, +`namespace`, `generateNamespace`, `x-forge-namespace` and nested `namespaces`. A plugin fails by throwing; forge stops and prints the message. Its files go through the same checks as forge's own: a path must be relative, use `/`, @@ -163,8 +168,8 @@ have no `.` or `..` parts, and no two files may share one. Things to know: -- **Each group runs in an instance of its own.** `generateGroup` sees - nothing that `generate` or another group kept in module state. +- **Each namespace runs in an instance of its own.** `generateNamespace` sees + nothing that `generate` or another namespace kept in module state. - **There is no Node.** Plugins have no `fs`, `process`, network or clock. Libraries in plain JavaScript bundle in and work. - **QuickJS runs the plugin.** `forge-plugin build` compiles it in, for a @@ -248,8 +253,8 @@ to the file. `Client`, which the file declares, isn't imported, so the other `Client` gets a `$`. `Pet` is used as both a type and a value, so it's imported once, as a value; `Temporal.Instant` is imported as `Temporal`. -A plugin puts these together. This one writes a `models.ts` for each group, -importing the models it refers to from the groups they're in: +A plugin puts these together. This one writes a `models.ts` for each namespace, +importing the models it refers to from the namespaces they're in: ```ts import { definePlugin, type Model, type Type } from '@cloudflare/forge/plugin'; @@ -262,7 +267,7 @@ function ts(type: Type): Part { case 'union': return join(' | ', type.members.map(ts)); case 'model': - // Imported from the models.ts of the model's group. + // Imported from the models.ts of the model's namespace. return frag`${ident.model(type.model).ty()}`; default: return frag`unknown`; @@ -279,10 +284,10 @@ function models(dir: Module, models: readonly Model[]): File { return f; } -export const { generate, generateGroup } = definePlugin({ - // The models in no group, which more than one top-level group uses. +export const { generate, generateNamespace } = definePlugin({ + // The models in no namespace, which more than one top-level namespace uses. generate: (forge) => [models(module.ROOT, forge.models)], - generateGroup: (group) => [models(module.group(group), group.models)], + generateNamespace: (namespace) => [models(module.namespace(namespace), namespace.models)], }); ``` @@ -299,8 +304,8 @@ export const { generate, generateGroup } = definePlugin({ - `module("pets/models.ts")` is a generated file, which other files import as `./models.js`, and `module("zod")` a package. `m.ident(name)`, or `ident(name, m)`, is an identifier it declares; `.ty()` marks it as a - type, and `.as(alias)` imports it under another name. `module.group(group)` - is a group's directory, `module.ROOT` the output directory's, and + type, and `.as(alias)` imports it under another name. `module.namespace(namespace)` + is a namespace's directory, `module.ROOT` the output directory's, and `ident.model(model)` a model's identifier. - `file(path)` returns a `File`, which is a plugin's file as it is: one with nothing printed isn't written, and its text starts with a preamble marking diff --git a/packages/forge/bin/forge-plugin.mjs b/packages/forge/bin/forge-plugin.mjs index 8adfe0c1b..05ba746d4 100644 --- a/packages/forge/bin/forge-plugin.mjs +++ b/packages/forge/bin/forge-plugin.mjs @@ -28,7 +28,7 @@ const usage = `usage: forge-plugin build [-o plugin.wasm] forge-plugin wit build compiles the plugin whose entry module is , a .ts or .js file - that exports definePlugin's generate and generateGroup, into a + that exports definePlugin's generate and generateNamespace, into a WebAssembly component with QuickJS. Run it with: forge openapi.json --target wasm://plugin.wasm --out sdk -o, --out the component to write (default: plugin.wasm) diff --git a/packages/forge/plugin/forge.wit b/packages/forge/plugin/forge.wit index 7a489f74e..f15557753 100644 --- a/packages/forge/plugin/forge.wit +++ b/packages/forge/plugin/forge.wit @@ -1,5 +1,5 @@ -package forge:plugin@0.3.0; +package forge:plugin@0.4.0; /// Read-only access to the forge a plugin runs on, which the host keeps. /// Each id is an index the host gave out. An id the host never gave out @@ -8,7 +8,7 @@ package forge:plugin@0.3.0; /// Everything a part has is returned in one call, and orders and meanings /// are as the Rust API documents them. interface forge { - type group-id = u32; + type namespace-id = u32; type method-id = u32; type model-id = u32; type type-id = u32; @@ -30,19 +30,19 @@ interface forge { /// How many of each part the forge has. Ids run from 0 to the count. record forge-counts { - groups: u32, + namespaces: u32, methods: u32, models: u32, types: u32, } - record group-info { + record namespace-info { name: name, description: string, - /// The groups from the top level down to this one, ending with it. - path: list, - /// The groups directly inside, sorted by name. - children: list, + /// The namespaces from the top level down to this one, ending with it. + path: list, + /// The namespaces directly inside, sorted by name. + children: list, /// The methods directly inside, sorted by name. methods: list, models: list, @@ -89,7 +89,7 @@ interface forge { record method-info { name: name, - group: group-id, + namespace: namespace-id, http-method: http-method, path: string, summary: string, @@ -117,7 +117,7 @@ interface forge { record model-info { schema-name: string, name: name, - group: option, + namespace: option, description: string, deprecated: bool, ty: type-id, @@ -191,11 +191,11 @@ interface forge { info: func() -> forge-info; counts: func() -> forge-counts; - /// Every group, depth first. - groups: func() -> list; - /// The models at the top level, in no group. + /// Every namespace, depth first. + namespaces: func() -> list; + /// The models at the top level, in no namespace. models: func() -> list; - get-group: func(id: group-id) -> group-info; + get-namespace: func(id: namespace-id) -> namespace-info; get-method: func(id: method-id) -> method-info; get-model: func(id: model-id) -> model-info; get-type: func(id: type-id) -> type-info; @@ -206,7 +206,7 @@ interface forge { world plugin { import forge; - use forge.{group-id}; + use forge.{namespace-id}; /// A generated file: its path relative to the output directory, using /// `/` as a separator, and its content. A file with no content is not @@ -218,7 +218,7 @@ world plugin { /// Generates files for the forge. Called exactly once. export generate: func() -> result, string>; - /// Generates files for a group. Called exactly once for each group, in - /// parallel with other groups, each in an instance of its own. - export generate-group: func(group: group-id) -> result, string>; + /// Generates files for a namespace. Called exactly once for each namespace, in + /// parallel with other namespaces, each in an instance of its own. + export generate-namespace: func(namespace: namespace-id) -> result, string>; } diff --git a/packages/forge/src/index.ts b/packages/forge/src/index.ts index d40d60c63..15f0ff41c 100644 --- a/packages/forge/src/index.ts +++ b/packages/forge/src/index.ts @@ -2,24 +2,24 @@ * Write forge plugins in TypeScript or JavaScript. * * A plugin generates files from the forge of an OpenAPI document: its - * groups, methods, models and types, as forge's Rust API has them. Build it + * namespaces, methods, models and types, as forge's Rust API has them. Build it * with `forge-plugin build`, and run it with * `forge openapi.json --target wasm://plugin.wasm --out sdk`. * * ```ts * import { definePlugin } from "@cloudflare/forge/plugin"; * - * export const { generate, generateGroup } = definePlugin({ - * generateGroup(group) { - * const names = group.methods.map((m) => m.name.camel()); - * return [{ path: `${group.name.kebab()}.txt`, content: names.join("\n") }]; + * export const { generate, generateNamespace } = definePlugin({ + * generateNamespace(namespace) { + * const names = namespace.methods.map((m) => m.name.camel()); + * return [{ path: `${namespace.name.kebab()}.txt`, content: names.join("\n") }]; * }, * }); * ``` * * The forge stays in forge's process. Each part is fetched the first time * a plugin reads it, and kept until the call ends, so reading a part again - * is free, and the same part is always the same object: compare groups, + * is free, and the same part is always the same object: compare namespaces, * methods and models with `===`. * * @module @@ -32,8 +32,8 @@ // classes behind them stay private, so the bindings never show in // index.d.ts. -import * as host from 'forge:plugin/forge@0.3.0'; -import type * as hostPlugin from 'forge:plugin/plugin@0.3.0'; +import * as host from 'forge:plugin/forge@0.4.0'; +import type * as hostPlugin from 'forge:plugin/plugin@0.4.0'; // --------------------------------------------------------------------------- // The API. @@ -43,11 +43,13 @@ export interface Plugin { /** Generates files for the forge. Called exactly once. */ generate?(forge: Forge): Iterable; /** - * Generates files for a group. Called exactly once for each group, in - * parallel with other groups, each in an instance of its own. Nothing a - * call keeps in module state is seen by the calls for other groups. + * Generates files for a namespace. Called exactly once for each namespace, in + * parallel with other namespaces, each in an instance of its own. Nothing a + * call keeps in module state is seen by the calls for other namespaces. */ - generateGroup?(group: Group): Iterable; + generateNamespace?(namespace: Namespace): Iterable; + /** @deprecated Use {@link generateNamespace} instead. */ + generateGroup?(namespace: Namespace): Iterable; } /** @@ -67,7 +69,7 @@ export interface File { /** What the entry module of a plugin exports, under these names. */ export interface Exports { generate(): Array<{ path: string; content: Uint8Array }>; - generateGroup(group: number): Array<{ path: string; content: Uint8Array }>; + generateNamespace(namespace: number): Array<{ path: string; content: Uint8Array }>; } /** @@ -102,36 +104,43 @@ export interface Forge { readonly description: string; /** The URL of the first server, with its variables substituted. */ readonly baseUrl: string; - /** Every group, depth first. Siblings are sorted by name. */ - readonly groups: readonly Group[]; - /** The models in no group: those methods in more than one top-level group use. */ + /** Every namespace, depth first. Siblings are sorted by name. */ + readonly namespaces: readonly Namespace[]; + /** @deprecated Use {@link namespaces} instead. */ + readonly groups: readonly Namespace[]; + /** The models in no namespace: those methods in more than one top-level namespace use. */ readonly models: readonly Model[]; } -/** A named collection of subgroups, methods and models. */ -export interface Group { +/** A named collection of subnamespaces, methods and models. */ +export interface Namespace { readonly name: Name; /** A short description, or an empty string. */ readonly description: string; - /** The names of the groups from the top level down to this one, ending with its own. */ + /** The names of the namespaces from the top level down to this one, ending with its own. */ readonly path: readonly Name[]; - /** The group this one is in, or undefined at the top level. */ - readonly parent: Group | undefined; - /** The groups directly inside, sorted by name. */ - readonly children: readonly Group[]; - /** Every group below this one, depth first. */ - readonly descendants: readonly Group[]; + /** The namespace this one is in, or undefined at the top level. */ + readonly parent: Namespace | undefined; + /** The namespaces directly inside, sorted by name. */ + readonly children: readonly Namespace[]; + /** Every namespace below this one, depth first. */ + readonly descendants: readonly Namespace[]; /** The methods directly inside, sorted by name. */ readonly methods: readonly Method[]; - /** The models placed directly in this group: the deepest one every method using them shares. */ + /** The models placed directly in this namespace: the deepest one every method using them shares. */ readonly models: readonly Model[]; } +/** @deprecated Use {@link Namespace} instead. */ +export type Group = Namespace; + /** An operation an SDK exposes as a function. */ export interface Method { - /** Unique within its group. */ + /** Unique within its namespace. */ readonly name: Name; - readonly group: Group; + readonly namespace: Namespace; + /** @deprecated Use {@link namespace} instead. */ + readonly group: Namespace; /** As it is sent, such as `GET`. */ readonly httpMethod: string; /** The path template, such as `/accounts/{account_id}`. */ @@ -245,8 +254,10 @@ export interface Model { /** Its key in `components.schemas`, such as `Account`, or a path such as `Tree/$defs/Node`. */ readonly schemaName: string; readonly name: Name; - /** The group it is placed in, or undefined at the top level. */ - readonly group: Group | undefined; + /** The namespace it is placed in, or undefined at the top level. */ + readonly namespace: Namespace | undefined; + /** @deprecated Use {@link namespace} instead. */ + readonly group: Namespace | undefined; readonly description: string; readonly deprecated: boolean; readonly type: Type; @@ -376,7 +387,7 @@ export interface ModelType { * plugin's entry module: * * ```ts - * export const { generate, generateGroup } = definePlugin({ ... }); + * export const { generate, generateNamespace } = definePlugin({ ... }); * ``` * * Throw to fail: forge stops and prints the error's message. @@ -386,10 +397,13 @@ export function definePlugin(plugin: Plugin): Exports { generate() { return call(() => files(plugin.generate?.(new ForgeImpl()) ?? [])); }, - generateGroup(id: host.GroupId) { - return call(() => files(plugin.generateGroup?.(new ForgeImpl().group(id)) ?? [])); + generateNamespace(id: host.NamespaceId) { + return call(() => { + const namespace = new ForgeImpl().namespace(id); + return files(plugin.generateNamespace?.(namespace) ?? plugin.generateGroup?.(namespace) ?? []); + }); }, - } satisfies Pick; + } satisfies Pick; return exports; } @@ -557,11 +571,11 @@ class ForgeImpl implements Forge { readonly version: string; readonly description: string; readonly baseUrl: string; - private readonly groupCache = new Map(); + private readonly namespaceCache = new Map(); private readonly methodCache = new Map(); private readonly modelCache = new Map(); private readonly typeCache = new Map(); - private readonly allGroups = lazy(() => ids(host.groups()).map((id) => this.group(id))); + private readonly allNamespaces = lazy(() => ids(host.namespaces()).map((id) => this.namespace(id))); private readonly topModels = lazy(() => ids(host.models()).map((id) => this.model(id))); constructor() { @@ -571,16 +585,20 @@ class ForgeImpl implements Forge { this.baseUrl = info.baseUrl; } - get groups(): readonly Group[] { - return this.allGroups(); + get namespaces(): readonly Namespace[] { + return this.allNamespaces(); + } + + get groups(): readonly Namespace[] { + return this.namespaces; } get models(): readonly Model[] { return this.topModels(); } - group(id: host.GroupId): GroupImpl { - return cached(this.groupCache, id, () => new GroupImpl(this, id)); + namespace(id: host.NamespaceId): NamespaceImpl { + return cached(this.namespaceCache, id, () => new NamespaceImpl(this, id)); } method(id: host.MethodId): MethodImpl { @@ -596,22 +614,22 @@ class ForgeImpl implements Forge { } } -class GroupImpl implements Group { +class NamespaceImpl implements Namespace { private readonly info: () => { name: Name; description: string; - path: host.GroupId[]; - children: host.GroupId[]; + path: host.NamespaceId[]; + children: host.NamespaceId[]; methods: host.MethodId[]; models: host.ModelId[]; }; constructor( private readonly forge: ForgeImpl, - id: host.GroupId, + id: host.NamespaceId, ) { this.info = lazy(() => { - const g = host.getGroup(id); + const g = host.getNamespace(id); return { name: new NameImpl(g.name), description: g.description, @@ -632,25 +650,25 @@ class GroupImpl implements Group { } get path(): readonly Name[] { - return this.info().path.map((id) => this.forge.group(id).name); + return this.info().path.map((id) => this.forge.namespace(id).name); } - get parent(): Group | undefined { + get parent(): Namespace | undefined { const path = this.info().path; const parent = path[path.length - 2]; - return parent === undefined ? undefined : this.forge.group(parent); + return parent === undefined ? undefined : this.forge.namespace(parent); } - get children(): readonly Group[] { - return this.info().children.map((id) => this.forge.group(id)); + get children(): readonly Namespace[] { + return this.info().children.map((id) => this.forge.namespace(id)); } - get descendants(): readonly Group[] { - const out: Group[] = []; + get descendants(): readonly Namespace[] { + const out: Namespace[] = []; const stack = [...this.children].reverse(); - for (let group = stack.pop(); group !== undefined; group = stack.pop()) { - out.push(group); - stack.push(...[...group.children].reverse()); + for (let namespace = stack.pop(); namespace !== undefined; namespace = stack.pop()) { + out.push(namespace); + stack.push(...[...namespace.children].reverse()); } return out; } @@ -681,7 +699,7 @@ const httpMethods: Record, string> = { class MethodImpl implements Method { private readonly info: () => { name: Name; - group: host.GroupId; + namespace: host.NamespaceId; httpMethod: string; path: string; summary: string; @@ -707,7 +725,7 @@ class MethodImpl implements Method { const body = opt(m.body); return { name: new NameImpl(m.name), - group: m.group, + namespace: m.namespace, httpMethod: m.httpMethod.tag === 'other' ? m.httpMethod.val : httpMethods[m.httpMethod.tag], path: m.path, summary: m.summary, @@ -734,8 +752,12 @@ class MethodImpl implements Method { return this.info().name; } - get group(): Group { - return this.forge.group(this.info().group); + get namespace(): Namespace { + return this.forge.namespace(this.info().namespace); + } + + get group(): Namespace { + return this.namespace; } get httpMethod(): string { @@ -867,7 +889,7 @@ class ModelImpl implements Model { private readonly info: () => { schemaName: string; name: Name; - group: host.GroupId | undefined; + namespace: host.NamespaceId | undefined; description: string; deprecated: boolean; ty: host.TypeId; @@ -882,7 +904,7 @@ class ModelImpl implements Model { return { schemaName: m.schemaName, name: new NameImpl(m.name), - group: opt(m.group), + namespace: opt(m.namespace), description: m.description, deprecated: m.deprecated, ty: m.ty, @@ -898,9 +920,13 @@ class ModelImpl implements Model { return this.info().name; } - get group(): Group | undefined { - const group = this.info().group; - return group === undefined ? undefined : this.forge.group(group); + get namespace(): Namespace | undefined { + const namespace = this.info().namespace; + return namespace === undefined ? undefined : this.forge.namespace(namespace); + } + + get group(): Namespace | undefined { + return this.namespace; } get description(): string { diff --git a/packages/forge/src/ts.ts b/packages/forge/src/ts.ts index 9115509e8..f6c38ea55 100644 --- a/packages/forge/src/ts.ts +++ b/packages/forge/src/ts.ts @@ -32,7 +32,7 @@ * ``` * * A `File` is a plugin's file as it is: return it from `generate` or - * `generateGroup`. One with nothing printed renders as nothing, so forge + * `generateNamespace`. One with nothing printed renders as nothing, so forge * doesn't write it. * * This is forge's Rust `forge::ts`, and renders the same text, with tagged @@ -46,7 +46,7 @@ // the forge only through the types of index.ts, so it needs no host, and // its tests run in Node. -import type { Group, Model, Name } from './index.js'; +import type { Model, Name, Namespace } from './index.js'; /** One level of indentation. */ const INDENT = ' '; @@ -253,17 +253,22 @@ export const module: { /** The output directory. Its `sub` modules are the top-level files. */ readonly ROOT: Module; /** - * Returns the directory that holds `group`'s files: the group's path, + * Returns the directory that holds `namespace`'s files: the namespace's path, * with each name in camelCase and prefixed with `_` if it starts with a - * digit. A group `list_all` inside `pet-store` gets `petStore/listAll`. + * digit. A namespace `list_all` inside `pet-store` gets `petStore/listAll`. * Top-level files go in `module.ROOT` instead. */ - group(group: Pick): Module; + namespace(namespace: Pick): Module; + /** @deprecated Use {@link namespace} instead. */ + group(namespace: Pick): Module; } = Object.freeze( Object.assign((path: string) => makeModule(path), { ROOT: makeModule(''), - group(group: Pick): Module { - return makeModule(group.path.map((name) => identifier(name.camel())).join('/')); + namespace(namespace: Pick): Module { + return makeModule(namespace.path.map((name) => identifier(name.camel())).join('/')); + }, + group(namespace: Pick): Module { + return module.namespace(namespace); }, }), ); @@ -274,13 +279,13 @@ export const ident: { /** * Returns the identifier of `model`: its name in PascalCase, prefixed * with `_` if it starts with a digit, declared in the `models.ts` of its - * group's directory (`module.group`), or of the root for a top-level + * namespace's directory (`module.namespace`), or of the root for a top-level * model. Call `ty` on it to import it with `import type`. * * A plugin that names models differently makes its own identifier, and * prints that one everywhere it refers to the model. */ - model(model: Pick): Ident; + model(model: Pick): Ident; } = Object.freeze( Object.assign( ( @@ -302,8 +307,8 @@ export const ident: { ); }, { - model(model: Pick): Ident { - const dir = model.group ? module.group(model.group) : module.ROOT; + model(model: Pick): Ident { + const dir = model.namespace ? module.namespace(model.namespace) : module.ROOT; return dir.sub('models.ts').ident(identifier(model.name.pascal())); }, }, @@ -525,14 +530,14 @@ function nameOf(state: FileState, id: Ident, aliases: Map): stri function declarationsOf(state: FileState, imports: Imports, aliases: Map): string { let b = ''; let previous: Module | undefined; - for (const group of chunkBy(imports.sorted, (a, b) => a.module.equals(b.module))) { - const module = group[0]!.module; + for (const namespace of chunkBy(imports.sorted, (a, b) => a.module.equals(b.module))) { + const module = namespace[0]!.module; if (previous && rank(previous) !== rank(module)) { b += '\n'; } previous = module; - const allTypes = group.every((i) => !imports.values.has(i.key)); - const names = group.map((i) => { + const allTypes = namespace.every((i) => !imports.values.has(i.key)); + const names = namespace.map((i) => { let name = i.name; const alias = aliases.get(i.key)!; if (alias !== name) { diff --git a/packages/forge/test/fixtures/metadata-host.js b/packages/forge/test/fixtures/metadata-host.js index f9a9b546e..328992632 100644 --- a/packages/forge/test/fixtures/metadata-host.js +++ b/packages/forge/test/fixtures/metadata-host.js @@ -16,12 +16,30 @@ export function info() { return { version: '1', description: '', baseUrl: '' }; } +export function namespaces() { + return [0]; +} + +export function getNamespace(id) { + if (id !== 0) { + throw new Error(`unknown namespace ${id}`); + } + return { name: ['pets'], description: '', path: [0], children: [], methods: [], models: [] }; +} + export function models() { return types.map((_, id) => id); } export function getModel(id) { - return { schemaName: `Type${id}`, name: [`type${id}`], description: '', deprecated: false, group: undefined, ty: id }; + return { + schemaName: `Type${id}`, + name: [`type${id}`], + description: '', + deprecated: false, + namespace: undefined, + ty: id, + }; } export function getType(id) { diff --git a/packages/forge/test/plugin.test.js b/packages/forge/test/plugin.test.js index be76a75f2..895992703 100644 --- a/packages/forge/test/plugin.test.js +++ b/packages/forge/test/plugin.test.js @@ -7,7 +7,7 @@ import { test } from 'node:test'; const hooks = registerHooks({ resolve(specifier, context, nextResolve) { return nextResolve( - specifier === 'forge:plugin/forge@0.3.0' + specifier === 'forge:plugin/forge@0.4.0' ? new URL('./fixtures/metadata-host.js', import.meta.url).href : specifier, context, @@ -17,6 +17,25 @@ const hooks = registerHooks({ const { definePlugin } = await import('../plugin/index.js'); hooks.deregister(); +test('deprecated group aliases forward to namespaces', () => { + let called = false; + const plugin = definePlugin({ + generate(forge) { + assert.equal(forge.groups, forge.namespaces); + return []; + }, + generateGroup(namespace) { + called = true; + assert.equal(namespace.name.toString(), 'pets'); + return []; + }, + }); + + plugin.generate(); + plugin.generateNamespace(0); + assert.equal(called, true); +}); + test('array bounds distinguish absent, zero and one through the plugin API', () => { definePlugin({ generate(forge) { diff --git a/packages/forge/test/ts.test.js b/packages/forge/test/ts.test.js index 4a4d1c8c0..da3e5615c 100644 --- a/packages/forge/test/ts.test.js +++ b/packages/forge/test/ts.test.js @@ -371,7 +371,7 @@ test('display', () => { }); test('models', () => { - // Stand-ins for the forge's groups and models: what module.group and + // Stand-ins for the forge's namespaces and models: what module.namespace and // ident.model read of them. const name = (...words) => ({ words, @@ -380,12 +380,18 @@ test('models', () => { }); const petStore = { path: [name('pet', 'store')] }; const listAll = { path: [name('pet', 'store'), name('list', 'all')] }; - const owner = { schemaName: 'pet_owner', name: name('pet', 'owner'), group: listAll, toString: () => 'pet_owner' }; - const twoFa = { schemaName: '2fa', name: name('2fa'), group: undefined, toString: () => '2fa' }; + const owner = { + schemaName: 'pet_owner', + name: name('pet', 'owner'), + namespace: listAll, + toString: () => 'pet_owner', + }; + const twoFa = { schemaName: '2fa', name: name('2fa'), namespace: undefined, toString: () => '2fa' }; assert.equal(module.ROOT.sub('models.ts').path, 'models.ts'); - assert.equal(module.group(petStore).path, 'petStore'); - assert.equal(module.group(listAll).sub('client.ts').path, 'petStore/listAll/client.ts'); + assert.equal(module.namespace(petStore).path, 'petStore'); + assert.equal(module.group(petStore).path, module.namespace(petStore).path); + assert.equal(module.namespace(listAll).sub('client.ts').path, 'petStore/listAll/client.ts'); assert.equal(ident.model(owner).module.path, 'petStore/listAll/models.ts'); assert.equal(ident.model(owner).name, 'PetOwner'); assert.equal(ident.model(twoFa).module.path, 'models.ts'); diff --git a/src/bin/forge/ts.rs b/src/bin/forge/ts.rs index 7e35e516c..3de110b18 100644 --- a/src/bin/forge/ts.rs +++ b/src/bin/forge/ts.rs @@ -1,13 +1,13 @@ //! The TypeScript SDK generator, ported from Go's `internal/ts` and //! following the specs in specs/ts. //! -//! Every group gets a client.ts, a models.ts and an index.ts in its +//! Every namespace gets a client.ts, a models.ts and an index.ts in its //! directory. The root also gets the runtime library, in `_/`. use std::borrow::Cow; use forge::ts::{self, Ident, Module, PREAMBLE}; -use forge::{Err, Forge, Group, Method, Model, Plugin}; +use forge::{Err, Forge, Method, Model, Namespace, Plugin}; mod client; mod codec; @@ -32,8 +32,8 @@ impl Plugin for Generator { Ok(files.into_iter()) } - fn generate_group(&self, group: Group<'_>) -> Result, Err> { - Ok(files(Node::Group(group))?.into_iter()) + fn generate_namespace(&self, namespace: Namespace<'_>) -> Result, Err> { + Ok(files(Node::Namespace(namespace))?.into_iter()) } } @@ -74,14 +74,14 @@ impl forge::File for File { } } -/// The root, or a group: what has a client.ts, models.ts and index.ts. -// TODO: Remove once the library exposes the root as a `Group`, which +/// The root, or a namespace: what has a client.ts, models.ts and index.ts. +// TODO: Remove once the library exposes the root as a `Namespace`, which // `Plugin::generate` could pass in. Then every function here takes a -// `Group`. +// `Namespace`. #[derive(Clone, Copy)] enum Node<'f> { Root(&'f Forge), - Group(Group<'f>), + Namespace(Namespace<'f>), } impl<'f> Node<'f> { @@ -89,24 +89,24 @@ impl<'f> Node<'f> { fn dir(self) -> Module { match self { Node::Root(_) => Module::ROOT, - Node::Group(group) => Module::group(group), + Node::Namespace(namespace) => Module::namespace(namespace), } } - /// Returns how many groups deep the node is: 0 for the root. + /// Returns how many namespaces deep the node is: 0 for the root. fn depth(self) -> usize { match self { Node::Root(_) => 0, - Node::Group(group) => group.path().len(), + Node::Namespace(namespace) => namespace.path().len(), } } - /// Returns the names of the groups down to the node, in camelCase and + /// Returns the names of the namespaces down to the node, in camelCase and /// separated by dots, for errors. fn path(self) -> String { match self { Node::Root(_) => String::new(), - Node::Group(group) => group.path().map(|name| name.camel()).collect::>().join("."), + Node::Namespace(namespace) => namespace.path().map(|name| name.camel()).collect::>().join("."), } } @@ -115,15 +115,15 @@ impl<'f> Node<'f> { fn methods(self) -> Vec> { match self { Node::Root(_) => Vec::new(), - Node::Group(group) => group.methods().collect(), + Node::Namespace(namespace) => namespace.methods().collect(), } } - /// Returns the groups directly in the node, sorted by name. - fn children(self) -> Vec> { + /// Returns the namespaces directly in the node, sorted by name. + fn children(self) -> Vec> { match self { - Node::Root(forge) => forge.groups().filter(|g| g.parent().is_none()).collect(), - Node::Group(group) => group.children().collect(), + Node::Root(forge) => forge.namespaces().filter(|g| g.parent().is_none()).collect(), + Node::Namespace(namespace) => namespace.children().collect(), } } @@ -132,7 +132,7 @@ impl<'f> Node<'f> { fn models(self) -> Vec> { let mut models: Vec> = match self { Node::Root(forge) => forge.models().collect(), - Node::Group(group) => group.models().collect(), + Node::Namespace(namespace) => namespace.models().collect(), }; models.sort_by_key(|m| m.schema_name()); models diff --git a/src/bin/forge/ts/client.rs b/src/bin/forge/ts/client.rs index f91d5adb4..28251d329 100644 --- a/src/bin/forge/ts/client.rs +++ b/src/bin/forge/ts/client.rs @@ -1,11 +1,11 @@ -//! The client.ts of a group: its client (specs/07-client-surface.md), and +//! The client.ts of a namespace: its client (specs/07-client-surface.md), and //! the requests (specs/05-requests.md), responses (specs/06-responses.md) //! and operations of its methods. use std::collections::{HashMap, HashSet}; use forge::ts::{File, Fragment, Ident, Module, frag, p}; -use forge::{Field, Group, Location, Method, Model, Type}; +use forge::{Field, Location, Method, Model, Namespace, Type}; use super::models::print_fields; use super::operation::operation; @@ -17,11 +17,12 @@ use super::{Node, request_options, runtime}; /// Returns the client.ts of `node`: its ClientOptions and Client, /// createClient and bind, the request and response types of each method, /// then the operation of each method. The library has already rejected -/// methods and groups with no name and duplicate methods, and equal group -/// names are one group. +/// methods and namespaces with no name and duplicate methods, and equal namespace +/// names are one namespace. pub(super) fn client(node: Node<'_>) -> File { - let groups = node.children(); - let group_names: HashMap> = groups.iter().map(|&sub| (group_name(sub), sub)).collect(); + let namespaces = node.children(); + let namespace_names: HashMap> = + namespaces.iter().map(|&sub| (namespace_name(sub), sub)).collect(); // A method that is not JSON is left out (specs/13-non-json-operations.md). let methods: Vec<(Method<'_>, Request<'_>)> = node @@ -34,7 +35,7 @@ pub(super) fn client(node: Node<'_>) -> File { let mut f = File::new(node.dir().sub("client.ts")); client_options(&mut f); p!(f); - client_interface(&mut f, &methods, &groups, &group_names); + client_interface(&mut f, &methods, &namespaces, &namespace_names); p!(f); p!( f, @@ -58,7 +59,7 @@ pub(super) fn client(node: Node<'_>) -> File { }); p!(f, "}"); p!(f); - bind(&mut f, &methods, &groups, &group_names); + bind(&mut f, &methods, &namespaces, &namespace_names); for (m, request) in &methods { p!(f); request_type(&mut f, *m, request); @@ -72,7 +73,7 @@ pub(super) fn client(node: Node<'_>) -> File { f } -/// Declares the ClientOptions of a group: the options every client takes, +/// Declares the ClientOptions of a namespace: the options every client takes, /// and baseUrl, which is required. fn client_options(f: &mut File) { p!( @@ -90,21 +91,21 @@ fn client_options(f: &mut File) { p!(f, "}"); } -/// Declares bind, which returns the Client of a group on a transport: each -/// method sends its operation, and each subgroup is a getter that binds the -/// subgroup's Client the first time it is read. A method and a subgroup +/// Declares bind, which returns the Client of a namespace on a transport: each +/// method sends its operation, and each subnamespace is a getter that binds the +/// subnamespace's Client the first time it is read. A method and a subnamespace /// with the same name share a getter, of the method with the members of the -/// subgroup's Client. +/// subnamespace's Client. fn bind( f: &mut File, methods: &[(Method<'_>, Request<'_>)], - groups: &[Group<'_>], - group_names: &HashMap>, + namespaces: &[Namespace<'_>], + namespace_names: &HashMap>, ) { let shared: HashMap> = methods .iter() .map(|&(m, _)| (method_name(m), m)) - .filter(|(name, _)| group_names.contains_key(name)) + .filter(|(name, _)| namespace_names.contains_key(name)) .collect(); p!( f, @@ -117,8 +118,8 @@ fn bind( " {" ); f.indent(|f| { - for &sub in groups { - let name = group_name(sub); + for &sub in namespaces { + let name = namespace_name(sub); match shared.get(&name) { Some(&m) => { let method = js_string(&method_name(m)); @@ -144,8 +145,8 @@ fn bind( p!(f, key(&name), ": (request, options) => ", send(f, m), ","); } } - for &sub in groups { - let name = group_name(sub); + for &sub in namespaces { + let name = namespace_name(sub); p!(f, "get ", key(&name), "() {"); f.indent(|f| match shared.get(&name) { Some(&m) => { @@ -182,29 +183,29 @@ fn send(f: &File, m: Method<'_>) -> Fragment { frag!("transport.send(", operation_ident(f, m), ", request, options)") } -/// Returns the bind of group `g`, imported under the name of the group. -fn bind_ident(g: Group<'_>) -> Ident { +/// Returns the bind of namespace `g`, imported under the name of the namespace. +fn bind_ident(g: Namespace<'_>) -> Ident { let alias = format!("bind{}", identifier(g.name().pascal())); - Module::group(g).sub("client.ts").ident("bind").alias(alias) + Module::namespace(g).sub("client.ts").ident("bind").alias(alias) } -/// Returns the Client of group `g`, imported under the name of the group. -fn client_ident(g: Group<'_>) -> Ident { +/// Returns the Client of namespace `g`, imported under the name of the namespace. +fn client_ident(g: Namespace<'_>) -> Ident { let alias = format!("{}Client", identifier(g.name().pascal())); - Module::group(g).sub("client.ts").ident("Client").ty().alias(alias) + Module::namespace(g).sub("client.ts").ident("Client").ty().alias(alias) } -/// Declares the Client of a group: its methods, then its subgroups, which -/// `group_names` has by name. A method and a subgroup with the same name +/// Declares the Client of a namespace: its methods, then its subnamespaces, which +/// `namespace_names` has by name. A method and a subnamespace with the same name /// share the property, which is a function with the members of the -/// subgroup's Client. +/// subnamespace's Client. fn client_interface( f: &mut File, methods: &[(Method<'_>, Request<'_>)], - groups: &[Group<'_>], - group_names: &HashMap>, + namespaces: &[Namespace<'_>], + namespace_names: &HashMap>, ) { - if methods.is_empty() && groups.is_empty() { + if methods.is_empty() && namespaces.is_empty() { p!(f, "export interface ", f.ident("Client"), " {}"); return; } @@ -216,7 +217,7 @@ fn client_interface( f.doc(&doc(&method_doc(m), m.is_deprecated())); let name = method_name(m); let params = method_params(f, m, request); - match group_names.get(&name) { + match namespace_names.get(&name) { None => p!(f, key(&name), "(", params, "): ", result(f, m), ";"), Some(&sub) => { p!( @@ -235,8 +236,8 @@ fn client_interface( } } } - for &sub in groups { - let name = group_name(sub); + for &sub in namespaces { + let name = namespace_name(sub); if !shared.contains(&name) { p!(f, "readonly ", key(&name), ": ", client_ident(sub), ";"); } @@ -475,12 +476,12 @@ pub(super) fn operation_ident(f: &File, m: Method<'_>) -> Ident { f.ident(format!("{}Operation", identifier(m.name().pascal()))) } -/// Returns the property of a method and a group in the Client. +/// Returns the property of a method and a namespace in the Client. fn method_name(m: Method<'_>) -> String { identifier(m.name().camel()) } -fn group_name(g: Group<'_>) -> String { +fn namespace_name(g: Namespace<'_>) -> String { identifier(g.name().camel()) } diff --git a/src/bin/forge/ts/index.rs b/src/bin/forge/ts/index.rs index 339960bc5..58344d82d 100644 --- a/src/bin/forge/ts/index.rs +++ b/src/bin/forge/ts/index.rs @@ -1,4 +1,4 @@ -//! The index.ts of a group, its public surface (specs/01-output-layout.md). +//! The index.ts of a namespace, its public surface (specs/01-output-layout.md). use std::collections::HashSet; diff --git a/src/bin/forge/ts/models.rs b/src/bin/forge/ts/models.rs index bcefc0f16..32302425c 100644 --- a/src/bin/forge/ts/models.rs +++ b/src/bin/forge/ts/models.rs @@ -1,4 +1,4 @@ -//! The models.ts of a group, following specs/04-model-declarations.md. +//! The models.ts of a namespace, following specs/04-model-declarations.md. use std::collections::HashMap; @@ -16,8 +16,8 @@ pub(super) fn models(node: Node<'_>) -> Result> { let models = node.models(); // TODO: Move both checks into the library, next to the method checks in // src/link.rs. It accepts a model whose name has no letters or digits, - // and two models in a group with equal names, such as schemas `Pet` and - // `pet`. Then this function, `files` and `generate_group` can't fail. + // and two models in a namespace with equal names, such as schemas `Pet` and + // `pet`. Then this function, `files` and `generate_namespace` can't fail. let mut errs = Vec::new(); let mut names: HashMap> = HashMap::new(); for &m in &models { @@ -29,7 +29,7 @@ pub(super) fn models(node: Node<'_>) -> Result> { )); } else if let Some(other) = names.get(&name) { errs.push(format!( - "group {:?}: schemas {:?} and {:?} have the same name {name:?}", + "namespace {:?}: schemas {:?} and {:?} have the same name {name:?}", node.path(), other.schema_name(), m.schema_name(), diff --git a/src/lib.rs b/src/lib.rs index f9ba83e9f..c3d07c578 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,15 +45,15 @@ pub struct Forge { /// with its server variables substituted. pub base_url: String, // Eventually we will support servers at descendants as well. - /// Every group, indexed by `GroupId`. - groups: Vec, - /// The top-level groups. - top: Vec, + /// Every namespace, indexed by `NamespaceId`. + namespaces: Vec, + /// The top-level namespaces. + top: Vec, /// Every method, indexed by `MethodId`. methods: Vec, /// Every model, indexed by `ModelId`. models: Vec, - /// The models at the top level, in no group. + /// The models at the top level, in no namespace. top_models: Vec, /// Every type, indexed by `TypeId`. types: Vec, @@ -77,22 +77,28 @@ pub struct Forge { } impl Forge { - /// Returns every group, depth first: each group comes before its - /// subgroups, and all of them before its next sibling. Siblings are + /// Returns every namespace, depth first: each namespace comes before its + /// subnamespaces, and all of them before its next sibling. Siblings are /// sorted by name. - pub fn groups(&self) -> impl Iterator> { + pub fn namespaces(&self) -> impl Iterator> { self.depth_first(&self.top) } - /// Returns the models at the top level, which are in no group, in an + /// Returns every namespace, depth first. + #[deprecated(note = "use `namespaces` instead")] + pub fn groups(&self) -> impl Iterator> { + self.namespaces() + } + + /// Returns the models at the top level, which are in no namespace, in an /// unspecified order. These are the models used by methods in more than - /// one top-level group. + /// one top-level namespace. pub fn models(&self) -> impl ExactSizeIterator> { self.top_models.iter().map(|&id| self.model(id)) } - fn group(&self, id: GroupId) -> Group<'_> { - Group { forge: self, id } + fn namespace(&self, id: NamespaceId) -> Namespace<'_> { + Namespace { forge: self, id } } fn method(&self, id: MethodId) -> Method<'_> { @@ -103,116 +109,120 @@ impl Forge { Model { forge: self, id } } - /// Returns the groups `ids` and every group inside them, depth first, - /// keeping the order of `ids` and of each group's subgroups. - fn depth_first<'f>(&'f self, ids: &'f [GroupId]) -> impl Iterator> { - // The groups still to visit, the next one last, so each list is + /// Returns the namespaces `ids` and every namespace inside them, depth first, + /// keeping the order of `ids` and of each namespace's subnamespaces. + fn depth_first<'f>(&'f self, ids: &'f [NamespaceId]) -> impl Iterator> { + // The namespaces still to visit, the next one last, so each list is // pushed in reverse. - let mut stack: Vec = ids.iter().rev().copied().collect(); + let mut stack: Vec = ids.iter().rev().copied().collect(); iter::from_fn(move || { - let group = self.group(stack.pop()?); - stack.extend(group.data().groups.iter().rev()); - Some(group) + let namespace = self.namespace(stack.pop()?); + stack.extend(namespace.data().namespaces.iter().rev()); + Some(namespace) }) } } -/// A named collection of subgroups, methods and models in a [`Forge`]. +/// A named collection of subnamespaces, methods and models in a [`Forge`]. /// -/// A `Group` is a reference into the `Forge` it came from: it is cheap to -/// copy, and cannot outlive the `Forge`. Two `Group`s are equal when they -/// are the same group of the same `Forge`. +/// A `Namespace` is a reference into the `Forge` it came from: it is cheap to +/// copy, and cannot outlive the `Forge`. Two `Namespace`s are equal when they +/// are the same namespace of the same `Forge`. #[derive(Clone, Copy)] -pub struct Group<'f> { +pub struct Namespace<'f> { forge: &'f Forge, - id: GroupId, + id: NamespaceId, } -impl<'f> Group<'f> { - /// Returns the name of the group. +/// The former name of [`Namespace`]. +#[deprecated(note = "use `Namespace` instead")] +pub type Group<'f> = Namespace<'f>; + +impl<'f> Namespace<'f> { + /// Returns the name of the namespace. pub fn name(self) -> &'f Name { &self.data().name } - /// Returns a short description of the group, or an empty string if it + /// Returns a short description of the namespace, or an empty string if it /// has none. /// /// Comes from the `x-forge-commands` extension at the root of the - /// document, which has an entry for each group, keyed by name. A - /// top-level group's description is the `description` of its entry. A - /// subgroup's entry sits in the `groups` of its parent's entry, or + /// document, which has an entry for each namespace, keyed by name. A + /// top-level namespace's description is the `description` of its entry. A + /// subnamespace's entry sits in the `namespaces` of its parent's entry, or /// anywhere above it under a key with dots: `a.b` in the root, or `b.c` - /// in the `groups` of `a`, both describe the group `c` in `b` in `a`. + /// in the `namespaces` of `a`, both describe the namespace `c` in `b` in `a`. /// Names match when they are equal as [`Name`]s. Two entries for the - /// same group are an error. + /// same namespace are an error. pub fn description(self) -> &'f str { &self.data().description } - /// Returns the names of the groups from the top level down to this one, - /// ending with this group's own name. + /// Returns the names of the namespaces from the top level down to this one, + /// ending with this namespace's own name. pub fn path(self) -> impl ExactSizeIterator + DoubleEndedIterator { self.data() .path .iter() - .map(move |&id| &self.forge.groups[id.index()].name) + .map(move |&id| &self.forge.namespaces[id.index()].name) } - /// Returns the group this one is inside, or `None` for a top-level - /// group. - pub fn parent(self) -> Option> { - // The path ends with this group, so its parent comes just before. + /// Returns the namespace this one is inside, or `None` for a top-level + /// namespace. + pub fn parent(self) -> Option> { + // The path ends with this namespace, so its parent comes just before. let [.., parent, _] = self.data().path[..] else { return None; }; - Some(self.forge.group(parent)) + Some(self.forge.namespace(parent)) } - /// Returns the groups directly inside this one, sorted by name. - pub fn children(self) -> impl ExactSizeIterator> { - self.data().groups.iter().map(move |&id| self.forge.group(id)) + /// Returns the namespaces directly inside this one, sorted by name. + pub fn children(self) -> impl ExactSizeIterator> { + self.data().namespaces.iter().map(move |&id| self.forge.namespace(id)) } - /// Returns every group below this one, depth first, walked the same way - /// as [`Forge::groups`]. This group is not included. - pub fn descendants(self) -> impl Iterator> { - self.forge.depth_first(&self.data().groups) + /// Returns every namespace below this one, depth first, walked the same way + /// as [`Forge::namespaces`]. This namespace is not included. + pub fn descendants(self) -> impl Iterator> { + self.forge.depth_first(&self.data().namespaces) } - /// Returns the methods directly in this group, sorted by name. + /// Returns the methods directly in this namespace, sorted by name. pub fn methods(self) -> impl ExactSizeIterator> { self.data().methods.iter().map(move |&id| self.forge.method(id)) } - /// Returns the models placed directly in this group, in an unspecified - /// order. See [`Model::group`] for where a model is placed. + /// Returns the models placed directly in this namespace, in an unspecified + /// order. See [`Model::namespace`] for where a model is placed. pub fn models(self) -> impl ExactSizeIterator> { self.data().models.iter().map(move |&id| self.forge.model(id)) } - fn data(self) -> &'f GroupData { - &self.forge.groups[self.id.index()] + fn data(self) -> &'f NamespaceData { + &self.forge.namespaces[self.id.index()] } } -impl PartialEq for Group<'_> { +impl PartialEq for Namespace<'_> { fn eq(&self, other: &Self) -> bool { std::ptr::eq(self.forge, other.forge) && self.id == other.id } } -impl Eq for Group<'_> {} +impl Eq for Namespace<'_> {} -impl std::hash::Hash for Group<'_> { +impl std::hash::Hash for Namespace<'_> { fn hash(&self, state: &mut H) { std::ptr::hash(self.forge, state); self.id.hash(state); } } -impl std::fmt::Debug for Group<'_> { +impl std::fmt::Debug for Namespace<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Group") + f.debug_struct("Namespace") .field("path", &self.path().collect::>()) .finish() } @@ -222,26 +232,26 @@ impl std::fmt::Debug for Group<'_> { /// exposes as a function. /// /// An operation can instead be exposed as several methods, each under its -/// own group and name, with the `x-forge-aliases` extension: an array of +/// own namespace 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 +/// operation's own namespace and name then make no method, and its own /// `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. /// /// An alias says, as an operation would, the method's -/// [`group`](Method::group), [`name`](Method::name), +/// [`namespace`](Method::namespace), [`name`](Method::name), /// [`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-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 +/// alias has it. Each alias is a method of its own namespace, so a model one /// uses is placed as though each alias were a separate operation. /// -/// Like a [`Group`], a `Method` is a cheap reference into its `Forge`. Two +/// Like a [`Namespace`], a `Method` is a cheap reference into its `Forge`. Two /// `Method`s are equal when they are the same method of the same `Forge`. #[derive(Clone, Copy)] pub struct Method<'f> { @@ -250,28 +260,34 @@ pub struct Method<'f> { } impl<'f> Method<'f> { - /// Returns the name of the method, which is unique within its group. + /// Returns the name of the method, which is unique within its namespace. /// /// 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 + /// with no letters or digits is an error. Two methods in a namespace with /// equal names are an error, whether they are aliases or not. pub fn name(self) -> &'f Name { &self.data().name } - /// Returns the group the method is in. + /// Returns the namespace the method is in. /// - /// Corresponds to the `x-forge-group-name` extension of the operation, or - /// of the alias the method comes from: a string of group names + /// Corresponds to the `x-forge-namespace` extension of the operation, or + /// of the alias the method comes from: a string of namespace names /// separated by dots, such as `accounts.members`, or an array with one - /// group name per element, top level first, whose elements are not + /// namespace 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 + /// with `x-forge-skip: true` is not a method. A namespace name with no /// letters or digits is an error. - pub fn group(self) -> Group<'f> { - self.forge.group(self.data().group) + pub fn namespace(self) -> Namespace<'f> { + self.forge.namespace(self.data().namespace) + } + + /// Returns the namespace the method is in. + #[deprecated(note = "use `namespace` instead")] + pub fn group(self) -> Namespace<'f> { + self.namespace() } /// Returns the HTTP method the operation is sent with. @@ -498,7 +514,7 @@ 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-forge-method-name`, - /// `x-forge-group-name`, `x-forge-skip`, `x-forge-availability`, + /// `x-forge-namespace`, `x-forge-skip`, `x-forge-availability`, /// `x-forge-hidden`, `x-forge-require-confirmation` and /// `x-forge-aliases`, are left out. Other keys forge does not read, such as /// the extensions other tools write, are kept, and `forge normalize` is @@ -538,7 +554,7 @@ impl std::hash::Hash for Method<'_> { impl std::fmt::Debug for Method<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Method") - .field("group", &self.group().path().collect::>()) + .field("namespace", &self.namespace().path().collect::>()) .field("name", self.name()) .field("http_method", self.http_method()) .field("path", &self.path()) @@ -633,7 +649,7 @@ impl Availability { /// parameter, one of [`Method::headers`]. Response headers are not read, /// so a use in one does not count. /// -/// Like a [`Group`], a `Model` is a cheap reference into its `Forge`. Two +/// Like a [`Namespace`], a `Model` is a cheap reference into its `Forge`. Two /// `Model`s are equal when they are the same model of the same `Forge`. #[derive(Clone, Copy)] pub struct Model<'f> { @@ -658,15 +674,21 @@ impl<'f> Model<'f> { &self.data().name } - /// Returns the group the model is placed in, or `None` if it is at the + /// Returns the namespace the model is placed in, or `None` if it is at the /// top level. /// - /// A model is placed in the deepest group shared by every method that + /// A model is placed in the deepest namespace shared by every method that /// uses it, directly or through other models: a model used in `a.b.c` /// and `a.b.d` is in `a.b`, one used only in `a.b.e` is in `a.b.e`, and /// one used in `a` and `x` is at the top level. - pub fn group(self) -> Option> { - Some(self.forge.group(self.data().group?)) + pub fn namespace(self) -> Option> { + Some(self.forge.namespace(self.data().namespace?)) + } + + /// Returns the namespace the model is placed in. + #[deprecated(note = "use `namespace` instead")] + pub fn group(self) -> Option> { + self.namespace() } /// Returns a description of the model, or an empty string if it has @@ -723,34 +745,37 @@ impl std::fmt::Display for Model<'_> { impl std::fmt::Debug for Model<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Model") - .field("group", &self.group().map(|group| group.path().collect::>())) + .field( + "namespace", + &self.namespace().map(|namespace| namespace.path().collect::>()), + ) .field("schema_name", &self.schema_name()) .finish() } } -/// The index of a group's data in its `Forge`. +/// The index of a namespace's data in its `Forge`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -struct GroupId(u32); +struct NamespaceId(u32); -impl GroupId { +impl NamespaceId { fn index(self) -> usize { self.0 as usize } } -/// What a [`Forge`] stores for each group. -struct GroupData { +/// What a [`Forge`] stores for each namespace. +struct NamespaceData { name: Name, description: String, - /// The groups directly inside this one. - groups: Vec, - /// The groups from the top level down to this one, ending with this - /// group itself. - path: Vec, - /// The methods directly in this group. + /// The namespaces directly inside this one. + namespaces: Vec, + /// The namespaces from the top level down to this one, ending with this + /// namespace itself. + path: Vec, + /// The methods directly in this namespace. methods: Vec, - /// The models placed directly in this group. + /// The models placed directly in this namespace. models: Vec, } @@ -767,8 +792,8 @@ impl MethodId { /// What a [`Forge`] stores for each method. struct MethodData { name: Name, - /// The group the method is in. - group: GroupId, + /// The namespace the method is in. + namespace: NamespaceId, http_method: HttpMethod, /// The path template, such as `/accounts/{account_id}`. path: Str, @@ -811,8 +836,8 @@ struct ModelData { /// Where the schema is under `components.schemas`. schema_name: Str, name: Name, - /// The group the model is placed in, or none for the top level. - group: Option, + /// The namespace the model is placed in, or none for the top level. + namespace: Option, description: Str, deprecated: bool, ty: TypeId, diff --git a/src/link.rs b/src/link.rs index 9760ff814..6ef0fb2dd 100644 --- a/src/link.rs +++ b/src/link.rs @@ -25,8 +25,8 @@ 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, + Availability, EnumValue, Forge, HttpMethod, Location, MethodData, MethodId, ModelData, ModelId, Name, + NamespaceData, NamespaceId, Number, Scalar, }; use openapi::{ Commands, ContentEncoding, Definition, Discriminator, Doc, Format, Info, Item, Literal, Operation, Parameter, @@ -99,7 +99,7 @@ fn build(mut doc: Doc) -> Result> { }; builder.add_paths(doc.paths); let Builder { - mut groups, + mut namespaces, mut top, methods, models, @@ -115,8 +115,8 @@ fn build(mut doc: Doc) -> Result> { if !errors.is_empty() { return Err(errors); } - sort(&mut groups, &mut top, &methods); - describe(&mut groups, &top, &mut commands); + sort(&mut namespaces, &mut top, &methods); + describe(&mut namespaces, &top, &mut commands); let mut names = vec![Name::default(); name_ids.len()]; for (name, id) in name_ids { names[id.index()] = name; @@ -125,7 +125,7 @@ fn build(mut doc: Doc) -> Result> { description: description(&info), version: info.version, base_url: url, - groups, + namespaces, top, methods, models, @@ -186,45 +186,45 @@ fn base_url(server: &Server) -> Result { Ok(url) } -/// Sorts the top-level groups `top`, and the subgroups and methods of each -/// group, by name. Only the lists of IDs are sorted, so every ID stays the +/// Sorts the top-level namespaces `top`, and the subnamespaces and methods of each +/// namespace, by name. Only the lists of IDs are sorted, so every ID stays the /// same. -fn sort(groups: &mut [GroupData], top: &mut [GroupId], methods: &[MethodData]) { - top.sort_by(|a, b| groups[a.index()].name.cmp(&groups[b.index()].name)); - for i in 0..groups.len() { +fn sort(namespaces: &mut [NamespaceData], top: &mut [NamespaceId], methods: &[MethodData]) { + top.sort_by(|a, b| namespaces[a.index()].name.cmp(&namespaces[b.index()].name)); + for i in 0..namespaces.len() { // Moved out while sorted, since the names are in the same slice. - let mut children = mem::take(&mut groups[i].groups); - children.sort_by(|a, b| groups[a.index()].name.cmp(&groups[b.index()].name)); - groups[i].groups = children; - groups[i] + let mut children = mem::take(&mut namespaces[i].namespaces); + children.sort_by(|a, b| namespaces[a.index()].name.cmp(&namespaces[b.index()].name)); + namespaces[i].namespaces = children; + namespaces[i] .methods .sort_by(|a, b| methods[a.index()].name.cmp(&methods[b.index()].name)); } } -/// Sets the description of the groups `ids` and every group inside them +/// Sets the description of the namespaces `ids` and every namespace inside them /// from their entries in `commands`. -fn describe(groups: &mut [GroupData], ids: &[GroupId], commands: &mut Commands) { +fn describe(namespaces: &mut [NamespaceData], ids: &[NamespaceId], commands: &mut Commands) { for &id in ids { - let data = &mut groups[id.index()]; + let data = &mut namespaces[id.index()]; let Some(command) = commands.0.get_mut(&data.name) else { continue; }; data.description = mem::take(&mut command.description); - // Moved out while the subgroups are described, since they live in + // Moved out while the subnamespaces are described, since they live in // the same slice. - let children = mem::take(&mut data.groups); - describe(groups, &children, &mut command.groups); - groups[id.index()].groups = children; + let children = mem::take(&mut data.namespaces); + describe(namespaces, &children, &mut command.namespaces); + namespaces[id.index()].namespaces = children; } } -/// Places each model in the deepest group shared by every method that uses +/// Places each model in the deepest namespace shared by every method that uses /// it, directly or through other models, and returns the models placed at /// the top level. fn place_models(forge: &mut Forge) -> Vec { let Forge { - groups, + namespaces, methods, models, types, @@ -241,16 +241,16 @@ fn place_models(forge: &mut Forge) -> Vec { models_in(&types, model.ty, &mut used, &mut walk); uses.push(used); } - // The groups shared by every use of each model so far, from the top + // The namespaces shared by every use of each model so far, from the top // level down; none for a model not yet reached. - let mut shared: Vec>> = vec![None; models.len()]; + let mut shared: Vec>> = vec![None; models.len()]; // The last method that reached each model, so a method walks each // model once. let mut reached_by: Vec = vec![usize::MAX; models.len()]; let mut stack = Vec::new(); for (i, method) in methods.iter().enumerate() { method_models(&types, method, &mut stack, &mut walk); - let path = &groups[method.group.index()].path; + let path = &namespaces[method.namespace.index()].path; while let Some(id) = stack.pop() { let m = id.index(); if reached_by[m] == i { @@ -272,13 +272,13 @@ fn place_models(forge: &mut Forge) -> Vec { let mut top = Vec::new(); for (m, shared) in shared.into_iter().enumerate() { let id = ModelId(m as u32); - let group = match shared { + let namespace = match shared { Some(shared) => shared.last().copied(), None => None, }; - models[m].group = group; - match group { - Some(group) => groups[group.index()].models.push(id), + models[m].namespace = namespace; + match namespace { + Some(namespace) => namespaces[namespace.index()].models.push(id), None => top.push(id), } } @@ -395,13 +395,13 @@ struct Builder<'d> { /// `$ref` are ignored. v30: bool, - groups: Vec, - top: Vec, - /// Each group by its parent, or none for a top-level group, and name. - group_index: HashMap<(Option, Name), GroupId>, + namespaces: Vec, + top: Vec, + /// Each namespace by its parent, or none for a top-level namespace, and name. + namespace_index: HashMap<(Option, Name), NamespaceId>, methods: Vec, - /// Each method by group and name. - method_index: HashMap<(GroupId, Name), MethodId>, + /// Each method by namespace and name. + method_index: HashMap<(NamespaceId, Name), MethodId>, types: Vec, /// The type of each scalar, which every use shares. scalars: Vec<(Scalar, TypeId)>, @@ -455,9 +455,9 @@ impl<'d> Builder<'d> { doc, index, v30, - groups: Vec::new(), + namespaces: Vec::new(), top: Vec::new(), - group_index: HashMap::new(), + namespace_index: HashMap::new(), methods: Vec::new(), method_index: HashMap::new(), types: Vec::new(), @@ -569,14 +569,14 @@ impl<'d> Builder<'d> { if definition.method_name.is_empty() { return; } - let group_names = doc.list(definition.group_names); - if group_names.is_empty() { + let namespace_names = doc.list(definition.namespace_names); + if namespace_names.is_empty() { return; } - let group_names: Vec = group_names.iter().map(|&name| Name::new(doc.str(name))).collect(); - for name in &group_names { + let namespace_names: Vec = namespace_names.iter().map(|&name| Name::new(doc.str(name))).collect(); + for name in &namespace_names { if name.is_empty() { - self.error("x-forge-group-name has a group name with no letters or digits"); + self.error("x-forge-namespace has a namespace name with no letters or digits"); return; } } @@ -595,19 +595,19 @@ impl<'d> Builder<'d> { )); } } - let mut group = None; - for group_name in group_names { - group = Some(self.child(group, group_name)); + let mut namespace = None; + for namespace_name in namespace_names { + namespace = Some(self.child(namespace, namespace_name)); } - let Some(group) = group else { + let Some(namespace) = namespace else { return; }; let id = MethodId(u32::try_from(self.methods.len()).expect("fewer than 2^32 methods")); - match self.method_index.entry((group, name.clone())) { + match self.method_index.entry((namespace, name.clone())) { Entry::Occupied(entry) => { let other = &self.methods[entry.get().index()]; let message = format!( - "method {:?} is also {} {} in the same group", + "method {:?} is also {} {} in the same namespace", name.words().join(" "), other.http_method.as_str(), self.strings.get(other.path) @@ -673,10 +673,10 @@ impl<'d> Builder<'d> { } let (responses, errors) = self.responses(operation); - self.groups[group.index()].methods.push(id); + self.namespaces[namespace.index()].methods.push(id); self.methods.push(MethodData { name, - group, + namespace, http_method, path: self.strings.push(path), summary: self.strings.push(doc.str(operation.summary)), @@ -696,33 +696,33 @@ impl<'d> Builder<'d> { }); } - /// Returns the group `name` inside `parent`, or at the top level if + /// Returns the namespace `name` inside `parent`, or at the top level if /// `parent` is none, adding it if it is missing. - fn child(&mut self, parent: Option, name: Name) -> GroupId { + fn child(&mut self, parent: Option, name: Name) -> NamespaceId { let key = (parent, name); - if let Some(&id) = self.group_index.get(&key) { + if let Some(&id) = self.namespace_index.get(&key) { return id; } let name = &key.1; - let id = GroupId(u32::try_from(self.groups.len()).expect("fewer than 2^32 groups")); + let id = NamespaceId(u32::try_from(self.namespaces.len()).expect("fewer than 2^32 namespaces")); let mut path = match parent { - Some(parent) => self.groups[parent.index()].path.clone(), + Some(parent) => self.namespaces[parent.index()].path.clone(), None => Vec::new(), }; path.push(id); - self.groups.push(GroupData { + self.namespaces.push(NamespaceData { name: name.clone(), description: String::new(), - groups: Vec::new(), + namespaces: Vec::new(), path, methods: Vec::new(), models: Vec::new(), }); match parent { - Some(parent) => self.groups[parent.index()].groups.push(id), + Some(parent) => self.namespaces[parent.index()].namespaces.push(id), None => self.top.push(id), } - self.group_index.insert(key, id); + self.namespace_index.insert(key, id); id } @@ -1426,7 +1426,7 @@ impl<'d> Builder<'d> { self.models.push(ModelData { schema_name: self.strings.push(&schema_name), name, - group: None, + namespace: None, description: self.strings.push(description), deprecated, ty: unknown, diff --git a/src/link/openapi.rs b/src/link/openapi.rs index 7b8af55de..364d981e8 100644 --- a/src/link/openapi.rs +++ b/src/link/openapi.rs @@ -242,9 +242,12 @@ pub(super) struct Operation { pub(super) struct Definition { /// The `x-forge-method-name`, or empty for none. pub(super) method_name: Str, - /// The `x-forge-group-name`: the group path, top level first, or + /// The `x-forge-namespace`: the namespace path, top level first, or /// empty for none. - pub(super) group_names: Span, + pub(super) namespace_names: Span, + /// Whether `namespace_names` came from the canonical key, which wins + /// over the deprecated `x-forge-group-name` alias in either order. + namespace_is_native: bool, /// `x-forge-skip`. pub(super) skip: bool, /// `x-forge-hidden`. @@ -482,7 +485,7 @@ pub(super) struct Discriminator { } /// The `x-forge-commands` extension of the OpenAPI Object: an entry for -/// each top-level group, by name. +/// each top-level namespace, by name. #[derive(Default)] pub(super) struct Commands(pub(super) HashMap); @@ -491,11 +494,11 @@ pub(super) struct Commands(pub(super) HashMap); pub(super) struct Command { #[serde(default)] pub(super) description: String, - /// The entries of the group's subgroups. - #[serde(default)] - pub(super) groups: Commands, - /// Whether an entry describes this group, rather than it only being on - /// the way to a group a dotted name describes. + /// The entries of the namespace's subnamespaces. + #[serde(default, alias = "groups")] + pub(super) namespaces: Commands, + /// Whether an entry describes this namespace, rather than it only being on + /// the way to a namespace a dotted name describes. #[serde(skip)] pub(super) described: bool, } @@ -851,17 +854,17 @@ impl Read for Types { } } -/// Reads an `x-forge-group-name`: a string split on dots, or an array of -/// strings that are not split. The empty string is no group. -struct GroupNames; +/// Reads an `x-forge-namespace`: a string split on dots, or an array of +/// strings that are not split. The empty string is no namespace. +struct NamespaceNames; -impl Read for GroupNames { +impl Read for NamespaceNames { type Out = Span; fn read<'de, D: Deserializer<'de>>(doc: &mut Doc, deserializer: D) -> Result, D::Error> { - struct GroupNamesVisitor<'a>(&'a mut Doc); + struct NamespaceNamesVisitor<'a>(&'a mut Doc); - impl<'de> Visitor<'de> for GroupNamesVisitor<'_> { + impl<'de> Visitor<'de> for NamespaceNamesVisitor<'_> { type Value = Span; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -882,7 +885,7 @@ impl Read for GroupNames { } } - deserializer.deserialize_any(GroupNamesVisitor(doc)) + deserializer.deserialize_any(NamespaceNamesVisitor(doc)) } } @@ -1186,7 +1189,8 @@ keys! { /// The keys of a [`Definition`], in an Operation Object or an alias. DefinitionKey { MethodName = "x-forge-method-name", - GroupNames = "x-forge-group-name", + NamespaceNames = "x-forge-namespace", + LegacyNamespaceNames = "x-forge-group-name", Skip = "x-forge-skip", Hidden = "x-forge-hidden", Confirmation = "x-forge-require-confirmation", @@ -1204,7 +1208,16 @@ impl Definition { ) -> 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::NamespaceNames => { + self.namespace_names = map.next_value_seed(seed::(doc))?; + self.namespace_is_native = true; + } + DefinitionKey::LegacyNamespaceNames => { + let names = map.next_value_seed(seed::(doc))?; + if !self.namespace_is_native { + self.namespace_names = names; + } + } DefinitionKey::Skip => self.skip = map.next_value()?, DefinitionKey::Hidden => self.hidden = map.next_value()?, DefinitionKey::Confirmation => { @@ -1784,9 +1797,9 @@ impl<'de> Deserialize<'de> for Count { } /// Reads `x-forge-commands`. A key with dots, such as -/// `dynamic-routing.deployments`, is a path: it describes the group +/// `dynamic-routing.deployments`, is a path: it describes the namespace /// `deployments` inside `dynamic-routing`, the same as nesting the entry in -/// the `groups` of a `dynamic-routing` entry. +/// the `namespaces` of a `dynamic-routing` entry. impl<'de> Deserialize<'de> for Commands { fn deserialize>(deserializer: D) -> Result { struct CommandsVisitor; @@ -1814,15 +1827,15 @@ impl<'de> Deserialize<'de> for Commands { } impl Commands { - /// Places `command` at `path`, adding the groups on the way that no - /// entry describes. Two entries that describe the same group, such as - /// `abuse_reports` and `abuse-reports`, or `a.b` and `b` in the `groups` + /// Places `command` at `path`, adding the namespaces on the way that no + /// entry describes. Two entries that describe the same namespace, such as + /// `abuse_reports` and `abuse-reports`, or `a.b` and `b` in the `namespaces` /// of `a`, are an error. fn insert(&mut self, path: &[Name], command: Command) -> Result<(), String> { let (name, rest) = path.split_first().expect("a key has at least one name"); let entry = self.0.entry(name.clone()).or_default(); if !rest.is_empty() { - return entry.groups.insert(rest, command); + return entry.namespaces.insert(rest, command); } if entry.described && command.described { let words = name.words().join(" "); @@ -1830,15 +1843,15 @@ impl Commands { } let Command { description, - groups, + namespaces, described, } = command; if described { entry.description = description; entry.described = true; } - for (name, group) in groups.0 { - entry.groups.insert(&[name], group)?; + for (name, namespace) in namespaces.0 { + entry.namespaces.insert(&[name], namespace)?; } Ok(()) } diff --git a/src/normalize/vendor/fern.rs b/src/normalize/vendor/fern.rs index 0176b6d18..a2c28acc3 100644 --- a/src/normalize/vendor/fern.rs +++ b/src/normalize/vendor/fern.rs @@ -5,7 +5,7 @@ use super::{Rename, Rule, Source}; pub(super) struct Fern; static METHOD_NAME: Rename = Rename::same("x-forge-method-name"); -static GROUP_NAME: Rename = Rename::group("x-forge-group-name"); +static NAMESPACE: Rename = Rename::namespace("x-forge-namespace"); static SKIP: Rename = Rename::same("x-forge-skip"); static AVAILABILITY: Rename = Rename::same("x-forge-availability"); static PARAMETER_NAME: Rename = Rename::same("x-forge-parameter-name"); @@ -14,7 +14,7 @@ static PROPERTY_NAME: Rename = Rename::same("x-forge-property-name"); static DEFINITION: [Rule; 4] = [ ("x-fern-sdk-method-name", &METHOD_NAME), - ("x-fern-sdk-group-name", &GROUP_NAME), + ("x-fern-sdk-group-name", &NAMESPACE), ("x-fern-ignore", &SKIP), ("x-fern-availability", &AVAILABILITY), ]; diff --git a/src/normalize/vendor/mod.rs b/src/normalize/vendor/mod.rs index 34000422e..f33f7df27 100644 --- a/src/normalize/vendor/mod.rs +++ b/src/normalize/vendor/mod.rs @@ -65,7 +65,7 @@ pub(super) struct Rename { #[derive(Clone, Copy, PartialEq, Eq)] enum Form { Same, - Group, + Namespace, BoolOrTargets, } @@ -77,10 +77,10 @@ impl Rename { } } - pub(super) const fn group(native: &'static str) -> Rename { + pub(super) const fn namespace(native: &'static str) -> Rename { Rename { native, - form: Form::Group, + form: Form::Namespace, } } @@ -96,7 +96,7 @@ impl Rename { } fn value(self: &Rename, value: Value) -> (Value, Option) { - let Form::Group = self.form else { + let Form::Namespace = self.form else { return (value, None); }; let Value::Array(names) = &value else { @@ -108,8 +108,8 @@ impl Rename { ( Value::String(path), Some(Detail::new( - "group path written as a dotted string", - "group paths written as dotted strings", + "namespace path written as a dotted string", + "namespace paths written as dotted strings", )), ) } diff --git a/src/plugin.rs b/src/plugin.rs index 913d30072..292eb0266 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -5,7 +5,7 @@ use std::path::{Component, Path}; use rayon::prelude::*; -use crate::{Forge, Group}; +use crate::{Forge, Namespace}; pub mod ts; #[cfg(feature = "wasm")] @@ -25,10 +25,17 @@ pub trait Plugin: Sync { Ok(std::iter::empty()) } - /// Generates files for the given group. + /// Generates files for the given namespace. /// - /// This will be called exactly once for each group and in parallel with other groups. - fn generate_group(&self, _: Group<'_>) -> Result, Err> { + /// This will be called exactly once for each namespace and in parallel with other namespaces. + fn generate_namespace(&self, namespace: Namespace<'_>) -> Result, Err> { + #[expect(deprecated, reason = "the default preserves old Plugin implementations")] + self.generate_group(namespace) + } + + /// Generates files for the given namespace. + #[deprecated(note = "use `generate_namespace` instead")] + fn generate_group(&self, _: Namespace<'_>) -> Result, Err> { Ok(std::iter::empty()) } } @@ -43,19 +50,19 @@ pub trait File: Send { /// Runs `plugin` on `forge` and returns every file it generates. /// -/// Calls [`Plugin::generate`] once, then [`Plugin::generate_group`] for each -/// group in parallel. The files from `generate` come first, then each -/// group's files in the order of [`Forge::groups`]. +/// Calls [`Plugin::generate`] once, then [`Plugin::generate_namespace`] for each +/// namespace in parallel. The files from `generate` come first, then each +/// namespace's files in the order of [`Forge::namespaces`]. pub fn run(forge: &Forge, plugin: &P) -> Result, Err> { let mut files: Vec = plugin.generate(forge)?.collect(); - let groups: Vec> = forge.groups().collect(); - let per_group = groups + let namespaces: Vec> = forge.namespaces().collect(); + let per_namespace = namespaces .par_iter() - .map(|&group| Ok(plugin.generate_group(group)?.collect::>())) + .map(|&namespace| Ok(plugin.generate_namespace(namespace)?.collect::>())) .collect::, Err>>()?; - files.extend(per_group.into_iter().flatten()); + files.extend(per_namespace.into_iter().flatten()); Ok(files) } diff --git a/src/plugin/ts.rs b/src/plugin/ts.rs index 1447dee62..481e0139f 100644 --- a/src/plugin/ts.rs +++ b/src/plugin/ts.rs @@ -17,7 +17,7 @@ use std::borrow::Cow; use std::fmt; -use crate::{Group, Model}; +use crate::{Model, Namespace}; mod file; mod print; @@ -103,17 +103,23 @@ impl Module { Module(Cow::Borrowed(path)) } - /// Returns the directory that holds `group`'s files, its - /// [`sub`](Module::sub) modules: the group's path, with each name in + /// Returns the directory that holds `namespace`'s files, its + /// [`sub`](Module::sub) modules: the namespace's path, with each name in /// camelCase and prefixed with `_` if it starts with a digit. Top-level /// files go in [`Module::ROOT`] instead. /// - /// A group `list_all` inside `pet-store` gets `petStore/listAll`. - pub fn group(group: Group<'_>) -> Module { - let dirs: Vec = group.path().map(|name| identifier(name.camel())).collect(); + /// A namespace `list_all` inside `pet-store` gets `petStore/listAll`. + pub fn namespace(namespace: Namespace<'_>) -> Module { + let dirs: Vec = namespace.path().map(|name| identifier(name.camel())).collect(); Module::from(dirs.join("/")) } + /// Returns the directory that holds `namespace`'s files. + #[deprecated(note = "use `namespace` instead")] + pub fn group(namespace: Namespace<'_>) -> Module { + Module::namespace(namespace) + } + /// Returns the path or specifier of the module. pub fn as_str(&self) -> &str { &self.0 @@ -189,7 +195,7 @@ pub struct Ident { impl Ident { /// Returns the identifier of `model`: its name in PascalCase, prefixed /// with `_` if it starts with a digit, declared in the `models.ts` of - /// its group's directory ([`Module::group`]), or of the root for a + /// its namespace's directory ([`Module::namespace`]), or of the root for a /// top-level model. Call [`ty`](Ident::ty) on it to import it with /// `import type`. /// @@ -197,7 +203,7 @@ impl Ident { /// changes the identifier and prints that one everywhere it refers to /// the model. pub fn model(model: Model<'_>) -> Ident { - let dir = model.group().map_or(Module::ROOT, Module::group); + let dir = model.namespace().map_or(Module::ROOT, Module::namespace); dir.sub("models.ts").ident(identifier(model.name().pascal())) } diff --git a/src/plugin/ts/tests.rs b/src/plugin/ts/tests.rs index 58ad0a549..8b113c5aa 100644 --- a/src/plugin/ts/tests.rs +++ b/src/plugin/ts/tests.rs @@ -370,25 +370,28 @@ fn models() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-forge-group-name": ["pet-store", "list_all"], "x-forge-method-name": "get", + "/a": {"get": {"x-forge-namespace": ["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-forge-group-name": ["pet-store"], "x-forge-method-name": "get", + "/b": {"get": {"x-forge-namespace": ["pet-store"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/2fa"}}}}}}}, - "/c": {"get": {"x-forge-group-name": ["other"], "x-forge-method-name": "get", + "/c": {"get": {"x-forge-namespace": ["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"}}} }"##, ) .unwrap(); - let group = |path: &[&str]| { + let namespace = |path: &[&str]| { let names: Vec = path.iter().map(|name| Name::new(name)).collect(); - forge.groups().find(|group| group.path().eq(names.iter())).unwrap() + forge + .namespaces() + .find(|namespace| namespace.path().eq(names.iter())) + .unwrap() }; let model = |schema_name: &str| { forge - .groups() - .flat_map(|group| group.models()) + .namespaces() + .flat_map(|namespace| namespace.models()) .chain(forge.models()) .find(|model| model.schema_name() == schema_name) .unwrap() @@ -396,9 +399,9 @@ fn models() { let (owner, two_fa) = (model("pet_owner"), model("2fa")); assert_eq!(Module::ROOT.sub("models.ts"), Module::new("models.ts")); - assert_eq!(Module::group(group(&["pet-store"])), Module::new("petStore")); + assert_eq!(Module::namespace(namespace(&["pet-store"])), Module::new("petStore")); assert_eq!( - Module::group(group(&["pet-store", "list_all"])).sub("client.ts"), + Module::namespace(namespace(&["pet-store", "list_all"])).sub("client.ts"), Module::new("petStore/listAll/client.ts") ); assert_eq!( diff --git a/src/plugin/wasm.rs b/src/plugin/wasm.rs index 6d5729cd7..3ab002140 100644 --- a/src/plugin/wasm.rs +++ b/src/plugin/wasm.rs @@ -2,14 +2,14 @@ //! targets the component model. //! //! A plugin is a component of the `plugin` world in [`WIT`]. It exports -//! `generate` and `generate-group`, which [`WasmPlugin`] calls where -//! [`Plugin::generate`] and [`Plugin::generate_group`] are, and imports +//! `generate` and `generate-namespace`, which [`WasmPlugin`] calls where +//! [`Plugin::generate`] and [`Plugin::generate_namespace`] are, and imports //! the `forge` interface, which reads the [`Forge`] the host runs it on. //! //! The forge is never copied into a plugin. It stays in the host, and the -//! plugin reads it a part at a time through ids: a group, method, model or -//! type is a `u32`, and the plugin calls `get-group`, `get-method`, -//! `get-model` or `get-type` for the part's data. Groups run in parallel, +//! plugin reads it a part at a time through ids: a namespace, method, model or +//! type is a `u32`, and the plugin calls `get-namespace`, `get-method`, +//! `get-model` or `get-type` for the part's data. Namespaces run in parallel, //! each in an instance of its own, and every instance reads the same //! forge. //! @@ -37,7 +37,7 @@ use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; use crate::arena::Str; use crate::method::{ContentData, ResponseData}; use crate::types::{EnumData, FieldData, TypeData}; -use crate::{Availability, Err, File, Forge, Group, HttpMethod, Json, Location, Name, Number, Plugin, Scalar}; +use crate::{Availability, Err, File, Forge, HttpMethod, Json, Location, Name, Namespace, Number, Plugin, Scalar}; /// Defines [`WIT`] and its host bindings from one copy of the text. macro_rules! wit { @@ -60,7 +60,7 @@ macro_rules! wit { wit!( r#" -package forge:plugin@0.3.0; +package forge:plugin@0.4.0; /// Read-only access to the forge a plugin runs on, which the host keeps. /// Each id is an index the host gave out. An id the host never gave out @@ -69,7 +69,7 @@ package forge:plugin@0.3.0; /// Everything a part has is returned in one call, and orders and meanings /// are as the Rust API documents them. interface forge { - type group-id = u32; + type namespace-id = u32; type method-id = u32; type model-id = u32; type type-id = u32; @@ -91,19 +91,19 @@ interface forge { /// How many of each part the forge has. Ids run from 0 to the count. record forge-counts { - groups: u32, + namespaces: u32, methods: u32, models: u32, types: u32, } - record group-info { + record namespace-info { name: name, description: string, - /// The groups from the top level down to this one, ending with it. - path: list, - /// The groups directly inside, sorted by name. - children: list, + /// The namespaces from the top level down to this one, ending with it. + path: list, + /// The namespaces directly inside, sorted by name. + children: list, /// The methods directly inside, sorted by name. methods: list, models: list, @@ -150,7 +150,7 @@ interface forge { record method-info { name: name, - group: group-id, + namespace: namespace-id, http-method: http-method, path: string, summary: string, @@ -178,7 +178,7 @@ interface forge { record model-info { schema-name: string, name: name, - group: option, + namespace: option, description: string, deprecated: bool, ty: type-id, @@ -252,11 +252,11 @@ interface forge { info: func() -> forge-info; counts: func() -> forge-counts; - /// Every group, depth first. - groups: func() -> list; - /// The models at the top level, in no group. + /// Every namespace, depth first. + namespaces: func() -> list; + /// The models at the top level, in no namespace. models: func() -> list; - get-group: func(id: group-id) -> group-info; + get-namespace: func(id: namespace-id) -> namespace-info; get-method: func(id: method-id) -> method-info; get-model: func(id: model-id) -> model-info; get-type: func(id: type-id) -> type-info; @@ -267,7 +267,7 @@ interface forge { world plugin { import forge; - use forge.{group-id}; + use forge.{namespace-id}; /// A generated file: its path relative to the output directory, using /// `/` as a separator, and its content. A file with no content is not @@ -279,9 +279,9 @@ world plugin { /// Generates files for the forge. Called exactly once. export generate: func() -> result, string>; - /// Generates files for a group. Called exactly once for each group, in - /// parallel with other groups, each in an instance of its own. - export generate-group: func(group: group-id) -> result, string>; + /// Generates files for a namespace. Called exactly once for each namespace, in + /// parallel with other namespaces, each in an instance of its own. + export generate-namespace: func(namespace: namespace-id) -> result, string>; } "# ); @@ -292,7 +292,7 @@ use bindings::forge::plugin::forge as wit; /// [`WIT`]. /// /// It compiles the component once, then keeps a pool of instances, so -/// groups run in parallel without compiling or instantiating it again. A +/// namespaces run in parallel without compiling or instantiating it again. A /// call takes an instance from the pool, or makes one if none is free, and /// returns it unless the plugin trapped. pub struct WasmPlugin { @@ -381,9 +381,9 @@ impl Plugin for WasmPlugin { self.call(forge, |plugin, store| plugin.call_generate(store)) } - fn generate_group(&self, group: Group<'_>) -> Result, Err> { - self.call(group.forge, |plugin, store| { - plugin.call_generate_group(store, group.id.0) + fn generate_namespace(&self, namespace: Namespace<'_>) -> Result, Err> { + self.call(namespace.forge, |plugin, store| { + plugin.call_generate_namespace(store, namespace.id.0) }) } } @@ -449,29 +449,29 @@ impl wit::Host for State { fn counts(&mut self) -> wasmtime::Result { let forge = self.forge()?; Ok(wit::ForgeCounts { - groups: forge.groups.len() as u32, + namespaces: forge.namespaces.len() as u32, methods: forge.methods.len() as u32, models: forge.models.len() as u32, types: forge.types.len() as u32, }) } - fn groups(&mut self) -> wasmtime::Result> { - Ok(self.forge()?.groups().map(|group| group.id.0).collect()) + fn namespaces(&mut self) -> wasmtime::Result> { + Ok(self.forge()?.namespaces().map(|namespace| namespace.id.0).collect()) } fn models(&mut self) -> wasmtime::Result> { Ok(self.forge()?.top_models.iter().map(|id| id.0).collect()) } - fn get_group(&mut self, id: u32) -> wasmtime::Result { + fn get_namespace(&mut self, id: u32) -> wasmtime::Result { let forge = self.forge()?; - let data = get(&forge.groups, id, "group")?; - Ok(wit::GroupInfo { + let data = get(&forge.namespaces, id, "namespace")?; + Ok(wit::NamespaceInfo { name: words(&data.name), description: data.description.clone(), path: data.path.iter().map(|id| id.0).collect(), - children: data.groups.iter().map(|id| id.0).collect(), + children: data.namespaces.iter().map(|id| id.0).collect(), methods: data.methods.iter().map(|id| id.0).collect(), models: data.models.iter().map(|id| id.0).collect(), }) @@ -483,7 +483,7 @@ impl wit::Host for State { let s = |s: Str| forge.strings.get(s).to_owned(); Ok(wit::MethodInfo { name: words(&data.name), - group: data.group.0, + namespace: data.namespace.0, http_method: match &data.http_method { HttpMethod::Get => wit::HttpMethod::Get, HttpMethod::Put => wit::HttpMethod::Put, @@ -535,7 +535,7 @@ impl wit::Host for State { Ok(wit::ModelInfo { schema_name: forge.strings.get(data.schema_name).to_owned(), name: words(&data.name), - group: data.group.map(|id| id.0), + namespace: data.namespace.map(|id| id.0), description: forge.strings.get(data.description).to_owned(), deprecated: data.deprecated, ty: data.ty.0, diff --git a/tests/cli.rs b/tests/cli.rs index 8926f77f0..4326bf65e 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-forge-group-name": ["a"], "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": ["a"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok"}}}}} }"#; diff --git a/tests/normalize/normalize_cli.rs b/tests/normalize/normalize_cli.rs index edc6de409..8ffbf0678 100644 --- a/tests/normalize/normalize_cli.rs +++ b/tests/normalize/normalize_cli.rs @@ -11,7 +11,7 @@ use serde_json::Value; const FERN: &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","responses":{"204":{"description":"ok"}}}}}}"#; /// The same document, written native. -const NATIVE: &str = r#"{"openapi":"3.0.3","info":{"title":"t","version":"1"},"paths":{"/a":{"get":{"x-forge-group-name":"a","x-forge-method-name":"get","responses":{"204":{"description":"ok"}}}}}}"#; +const NATIVE: &str = r#"{"openapi":"3.0.3","info":{"title":"t","version":"1"},"paths":{"/a":{"get":{"x-forge-namespace":"a","x-forge-method-name":"get","responses":{"204":{"description":"ok"}}}}}}"#; /// A document with a key from each compatibility source. const SOURCES: &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","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/S"}}}}}}}},"components":{"schemas":{"S":{"type":"object","properties":{"secret":{"type":"string","x-sensitive":true},"hidden":{"type":"string","x-stainless-skip":true}}}}}}"##; @@ -221,7 +221,7 @@ fn a_native_key_wins_and_the_other_is_dropped() { let file = dir.join("spec.json"); fs::write( &file, - r#"{"openapi":"3.0.3","info":{"title":"t","version":"1"},"paths":{"/a":{"get":{"x-fern-sdk-method-name":"old","x-forge-method-name":"new","x-forge-group-name":["a"],"responses":{"204":{"description":"ok"}}}}}}"#, + r#"{"openapi":"3.0.3","info":{"title":"t","version":"1"},"paths":{"/a":{"get":{"x-fern-sdk-method-name":"old","x-forge-method-name":"new","x-forge-namespace":["a"],"responses":{"204":{"description":"ok"}}}}}}"#, ) .unwrap(); @@ -234,7 +234,7 @@ fn a_native_key_wins_and_the_other_is_dropped() { } #[test] -fn a_group_that_cannot_be_a_dotted_string_is_kept_and_nothing_is_said_about_it() { +fn a_namespace_that_cannot_be_a_dotted_string_is_kept_and_nothing_is_said_about_it() { let dir = dir("kept"); let file = dir.join("spec.json"); fs::write(&file, FERN.replace(r#"["a"]"#, r#"["a.b"]"#)).unwrap(); @@ -249,7 +249,7 @@ fn a_group_that_cannot_be_a_dotted_string_is_kept_and_nothing_is_said_about_it() ); assert_eq!( fs::read_to_string(&file).unwrap(), - NATIVE.replace(r#""x-forge-group-name":"a""#, r#""x-forge-group-name":["a.b"]"#), + NATIVE.replace(r#""x-forge-namespace":"a""#, r#""x-forge-namespace":["a.b"]"#), "the array is kept, and only the keys are renamed" ); } @@ -617,7 +617,7 @@ fn standard_input_is_called_stdin_and_a_failure_on_it_says_so() { } #[test] -fn v_says_how_many_group_paths_were_joined() { +fn v_says_how_many_namespace_paths_were_joined() { let dir = dir("joined"); let file = dir.join("spec.json"); fs::write(&file, FERN).unwrap(); @@ -625,7 +625,7 @@ fn v_says_how_many_group_paths_were_joined() { let out = rewrite(&["-v", file.to_str().unwrap()], ""); assert!( - text(&out.stderr).contains(" 1 group path written as a dotted string"), + text(&out.stderr).contains(" 1 namespace path written as a dotted string"), "{}", text(&out.stderr) ); diff --git a/tests/parse/aliases.rs b/tests/parse/aliases.rs index f0d7db39d..438f7bc61 100644 --- a/tests/parse/aliases.rs +++ b/tests/parse/aliases.rs @@ -1,6 +1,6 @@ use forge::{Availability, Forge, HttpMethod, Json, Name, Scalar}; -use crate::common::{field_names, group, intersection, method, model, object, parse, parse_err, scalar}; +use crate::common::{field_names, intersection, method, model, namespace, object, parse, parse_err, scalar}; /// Returns a document with one operation, `PATCH /a/{id}/status`, whose own /// method would be `a.status.edit`, with `x-forge-aliases` written as @@ -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-forge-group-name": ["a", "status"], "x-forge-method-name": "edit", + "x-forge-namespace": ["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"}}}}], @@ -24,13 +24,13 @@ fn aliased(aliases: &str, fields: &str) -> String { ) } -/// Returns every method, as its group path and name joined by dots in -/// snake case, such as `a.pause`, in the order the groups are walked. +/// Returns every method, as its namespace path and name joined by dots in +/// snake case, such as `a.pause`, in the order the namespaces are walked. fn methods(forge: &Forge) -> Vec { let mut paths = Vec::new(); - for group in forge.groups() { - for method in group.methods() { - let mut path: Vec = group.path().map(|name| name.snake()).collect(); + for namespace in forge.namespaces() { + for method in namespace.methods() { + let mut path: Vec = namespace.path().map(|name| name.snake()).collect(); path.push(method.name().snake()); paths.push(path.join(".")); } @@ -39,8 +39,8 @@ fn methods(forge: &Forge) -> Vec { } const PAUSE_AND_TERMINATE: &str = r#"[ - {"x-forge-group-name": "a", "x-forge-method-name": "pause"}, - {"x-forge-group-name": ["a"], "x-forge-method-name": "terminate"} + {"x-forge-namespace": "a", "x-forge-method-name": "pause"}, + {"x-forge-namespace": ["a"], "x-forge-method-name": "terminate"} ]"#; #[test] @@ -51,23 +51,26 @@ fn each_alias_is_a_method_and_the_operation_is_not() { } #[test] -fn an_alias_group_is_split_on_dots_only_when_it_is_a_string() { +fn an_alias_namespace_is_split_on_dots_only_when_it_is_a_string() { let forge = parse(&aliased( r#"[ - {"x-forge-group-name": "a.b", "x-forge-method-name": "pause"}, - {"x-forge-group-name": ["a.b"], "x-forge-method-name": "stop"} + {"x-forge-namespace": "a.b", "x-forge-method-name": "pause"}, + {"x-forge-namespace": ["a.b"], "x-forge-method-name": "stop"} ]"#, "", )); - let in_group = |names: &[&str], method: &str| { + let in_namespace = |names: &[&str], method: &str| { let want: Vec = names.iter().map(|name| Name::new(name)).collect(); - let group = forge.groups().find(|group| group.path().eq(want.iter())).unwrap(); - group.methods().any(|m| *m.name() == Name::new(method)) + let namespace = forge + .namespaces() + .find(|namespace| namespace.path().eq(want.iter())) + .unwrap(); + namespace.methods().any(|m| *m.name() == Name::new(method)) }; - assert!(in_group(&["a", "b"], "pause"), "a string is two groups"); - assert!(in_group(&["a.b"], "stop"), "an array's name is one group"); - assert!(!in_group(&["a", "b"], "stop")); + assert!(in_namespace(&["a", "b"], "pause"), "a string is two namespaces"); + assert!(in_namespace(&["a.b"], "stop"), "an array's name is one namespace"); + assert!(!in_namespace(&["a", "b"], "stop")); } #[test] @@ -97,10 +100,10 @@ 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-forge-group-name": "a", "x-forge-method-name": "pause", + {"x-forge-namespace": "a", "x-forge-method-name": "pause", "x-forge-availability": "generally-available", "x-forge-hidden": true, "x-forge-params": {"status": {"default": "pause", "hidden": true}}}, - {"x-forge-group-name": "a", "x-forge-method-name": "terminate", + {"x-forge-namespace": "a", "x-forge-method-name": "terminate", "x-forge-availability": "beta", "x-forge-require-confirmation": "Sure?", "x-a": 1, "x-b": 2} ]"#, "", @@ -131,7 +134,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "pause"}]"#, + r#"[{"x-forge-namespace": "a", "x-forge-method-name": "pause"}]"#, r#""x-forge-availability": "beta", "x-forge-hidden": true, "x-forge-require-confirmation": "Sure?", "x-forge-params": {"status": {}},"#, )); @@ -145,13 +148,13 @@ fn alias_does_not_fall_back_to_the_operation() { } #[test] -fn alias_without_a_group_or_name_is_not_a_method() { +fn alias_without_a_namespace_or_name_is_not_a_method() { let forge = parse(&aliased( r#"[ - {"x-forge-group-name": "a", "x-forge-method-name": "pause"}, - {"x-forge-group-name": "a"}, + {"x-forge-namespace": "a", "x-forge-method-name": "pause"}, + {"x-forge-namespace": "a"}, {"x-forge-method-name": "resume"}, - {"x-forge-group-name": [], "x-forge-method-name": "restart"} + {"x-forge-namespace": [], "x-forge-method-name": "restart"} ]"#, "", )); @@ -162,7 +165,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-forge-group-name": "a", "x-forge-method-name": "pause"}]"#, + r#"["pause", null, 1, true, ["a"], {"x-forge-namespace": "a", "x-forge-method-name": "pause"}]"#, "", )); @@ -179,7 +182,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-forge-group-name": "a", "x-forge-method-name": "pause"}"#, + r#"{"x-forge-namespace": "a", "x-forge-method-name": "pause"}"#, "", )); @@ -192,11 +195,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-forge-group-name": "a", "x-forge-method-name": "pause", + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "pause", "responses": {"204": {"description": "ok"}}}}, "/b": {"post": {"x-forge-aliases": [ - {"x-forge-group-name": "a", "x-forge-method-name": "resume"}, - {"x-forge-group-name": "a", "x-forge-method-name": "Pause"} + {"x-forge-namespace": "a", "x-forge-method-name": "resume"}, + {"x-forge-namespace": "a", "x-forge-method-name": "Pause"} ], "responses": {"204": {"description": "ok"}}}} } }"#, @@ -212,8 +215,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-forge-group-name": "a", "x-forge-method-name": "pause"}, - {"x-forge-group-name": "a", "x-forge-method-name": "pause"} + {"x-forge-namespace": "a", "x-forge-method-name": "pause"}, + {"x-forge-namespace": "a", "x-forge-method-name": "pause"} ]"#, "", )); @@ -225,14 +228,14 @@ fn two_aliases_with_equal_names_are_an_error() { } #[test] -fn alias_group_name_without_letters_is_an_error_naming_the_alias() { +fn alias_namespace_name_without_letters_is_an_error_naming_the_alias() { let err = parse_err(&aliased( - r#"[{"x-forge-group-name": ["a", "", "b"], "x-forge-method-name": "pause"}]"#, + r#"[{"x-forge-namespace": ["a", "", "b"], "x-forge-method-name": "pause"}]"#, "", )); assert!( - err.contains("PATCH /a/{id}/status: x-forge-aliases[0]: x-forge-group-name"), + err.contains("PATCH /a/{id}/status: x-forge-aliases[0]: x-forge-namespace"), "{err}" ); } @@ -240,7 +243,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "pause", "x-forge-availability": "ga"}]"#, + r#"[1, {"x-forge-namespace": "a", "x-forge-method-name": "pause", "x-forge-availability": "ga"}]"#, "", )); @@ -271,8 +274,8 @@ fn skipped_operation_still_has_its_aliases() { fn skipped_alias_is_not_a_method() { let forge = parse(&aliased( r#"[ - {"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} + {"x-forge-namespace": "a", "x-forge-method-name": "pause", "x-forge-skip": true}, + {"x-forge-namespace": "a", "x-forge-method-name": "terminate", "x-forge-skip": false} ]"#, "", )); @@ -283,8 +286,8 @@ fn skipped_alias_is_not_a_method() { #[test] fn aliases_are_not_extensions() { let forge = parse(&aliased( - 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"}]}]"#, + r#"[{"x-forge-namespace": "a", "x-forge-method-name": "pause", "x-a": 1, + "x-forge-aliases": [{"x-forge-namespace": "b", "x-forge-method-name": "nested"}]}]"#, "", )); @@ -297,7 +300,7 @@ fn aliases_are_not_extensions() { #[test] fn keys_of_an_alias_that_are_not_extensions_are_ignored() { let forge = parse(&aliased( - r#"[{"x-forge-group-name": "a", "x-forge-method-name": "pause", + r#"[{"x-forge-namespace": "a", "x-forge-method-name": "pause", "summary": "Pause", "deprecated": false, "path": "/b"}]"#, "", )); @@ -313,7 +316,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -322,41 +325,41 @@ fn operation_without_aliases_keeps_its_own_method() { } #[test] -fn model_used_by_aliases_in_two_top_level_groups_is_at_the_top_level() { +fn model_used_by_aliases_in_two_top_level_namespaces_is_at_the_top_level() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/m": {"get": { - "x-forge-group-name": ["a", "b"], "x-forge-method-name": "get", + "x-forge-namespace": ["a", "b"], "x-forge-method-name": "get", "x-forge-aliases": [ - {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "get"}, - {"x-forge-group-name": ["x"], "x-forge-method-name": "get"} + {"x-forge-namespace": ["a", "b"], "x-forge-method-name": "get"}, + {"x-forge-namespace": ["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"}}}}} }"##, ); - assert_eq!(model(&forge, "M").group(), None); + assert_eq!(model(&forge, "M").namespace(), None); assert_eq!(forge.models().count(), 1); - assert_eq!(group(&forge, "a.b").models().count(), 0); + assert_eq!(namespace(&forge, "a.b").models().count(), 0); } #[test] -fn model_used_by_aliases_in_one_group_is_in_that_group() { +fn model_used_by_aliases_in_one_namespace_is_in_that_namespace() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/m": {"get": { - "x-forge-group-name": "x", "x-forge-method-name": "get", + "x-forge-namespace": "x", "x-forge-method-name": "get", "x-forge-aliases": [ - {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "get"}, - {"x-forge-group-name": ["a", "c"], "x-forge-method-name": "get"} + {"x-forge-namespace": ["a", "b"], "x-forge-method-name": "get"}, + {"x-forge-namespace": ["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"}}}}} }"##, ); - assert_eq!(model(&forge, "M").group(), Some(group(&forge, "a"))); + assert_eq!(model(&forge, "M").namespace(), Some(namespace(&forge, "a"))); } diff --git a/tests/parse/bodies.rs b/tests/parse/bodies.rs index e1adc96ba..966705094 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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"patch": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {{"/a": {{"post": {{"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 6bb8510a1..332549641 100644 --- a/tests/parse/common.rs +++ b/tests/parse/common.rs @@ -2,7 +2,7 @@ //! asserts only what it is about. Each helper panics with what there is //! when it finds nothing. -use forge::{EnumValue, Field, Forge, Group, Intersection, Method, Model, Name, Object, Scalar, Type, Union}; +use forge::{EnumValue, Field, Forge, Intersection, Method, Model, Name, Namespace, Object, Scalar, Type, Union}; /// Parses `json` as a whole document. #[track_caller] @@ -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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {{"/a": {{"get": {{"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {{"/a": {{"get": {{"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {{"200": {{"description": "ok", "content": {{"application/json": {{"schema": {schema}}}}}}}}}}}}}}}, "components": {{"schemas": {schemas}}} }}"# @@ -47,28 +47,28 @@ pub fn parse_err(json: &str) -> String { } } -/// Returns the group at `path`: the names from the top level down, +/// Returns the namespace at `path`: the names from the top level down, /// separated by dots, such as `zones` or `zones.settings`. #[track_caller] -pub fn group<'f>(forge: &'f Forge, path: &str) -> Group<'f> { +pub fn namespace<'f>(forge: &'f Forge, path: &str) -> Namespace<'f> { let names: Vec = path.split('.').map(Name::new).collect(); - let Some(group) = forge.groups().find(|group| group.path().eq(names.iter())) else { - panic!("no group {path:?} in {:?}", forge.groups().collect::>()); + let Some(namespace) = forge.namespaces().find(|namespace| namespace.path().eq(names.iter())) else { + panic!("no namespace {path:?} in {:?}", forge.namespaces().collect::>()); }; - group + namespace } -/// Returns the method at `path`: its group's path, a dot and its name, +/// Returns the method at `path`: its namespace's path, a dot and its name, /// such as `zones.get`. #[track_caller] pub fn method<'f>(forge: &'f Forge, path: &str) -> Method<'f> { - let Some((group_path, name)) = path.rsplit_once('.') else { - panic!("method path {path:?} is not group.method"); + let Some((namespace_path, name)) = path.rsplit_once('.') else { + panic!("method path {path:?} is not namespace.method"); }; - let group = group(forge, group_path); + let namespace = namespace(forge, namespace_path); let name = Name::new(name); - let Some(method) = group.methods().find(|method| *method.name() == name) else { - panic!("no method {path:?} in {:?}", group.methods().collect::>()); + let Some(method) = namespace.methods().find(|method| *method.name() == name) else { + panic!("no method {path:?} in {:?}", namespace.methods().collect::>()); }; method } @@ -77,7 +77,7 @@ pub fn method<'f>(forge: &'f Forge, path: &str) -> Method<'f> { pub fn models(forge: &Forge) -> Vec<&str> { let mut names: Vec<&str> = forge .models() - .chain(forge.groups().flat_map(|group| group.models())) + .chain(forge.namespaces().flat_map(|namespace| namespace.models())) .map(|model| model.schema_name()) .collect(); names.sort_unstable(); @@ -88,7 +88,11 @@ pub fn models(forge: &Forge) -> Vec<&str> { /// placed. #[track_caller] pub fn model<'f>(forge: &'f Forge, schema_name: &str) -> Model<'f> { - let models = || forge.models().chain(forge.groups().flat_map(|group| group.models())); + let models = || { + forge + .models() + .chain(forge.namespaces().flat_map(|namespace| namespace.models())) + }; let Some(model) = models().find(|model| model.schema_name() == schema_name) else { panic!("no model {schema_name:?} in {:?}", models().collect::>()); }; diff --git a/tests/parse/document.rs b/tests/parse/document.rs index 894ab6083..c0d6226c1 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {{"/a": {{"get": {{"x-forge-namespace": "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-forge-group-name": ["a", "", "b"], "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": ["a", "", "b"], "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); diff --git a/tests/parse/extensions.rs b/tests/parse/extensions.rs index 846791a71..88fe4a0b1 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-forge-group-name": "a", "x-forge-method-name": "get", + "x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# ) @@ -89,7 +89,7 @@ fn extensions_forge_reads_are_not_kept() { let get = method(&forge, "a.get"); assert_eq!(names(&forge, "a.get"), ["x-a"]); - assert_eq!(get.extension("x-forge-group-name"), None); + assert_eq!(get.extension("x-forge-namespace"), None); assert_eq!(get.extension("x-forge-skip"), None); } @@ -133,16 +133,16 @@ fn extensions_forge_reads_are_still_read() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-forge-group-name": ["a", "b"], "x-forge-method-name": "list", "x-a": 1, + "get": {"x-forge-namespace": ["a", "b"], "x-forge-method-name": "list", "x-a": 1, "responses": {"204": {"description": "ok"}}}, - "post": {"x-forge-group-name": "a", "x-forge-method-name": "create", "x-forge-skip": true, + "post": {"x-forge-namespace": "a", "x-forge-method-name": "create", "x-forge-skip": true, "x-a": 1, "responses": {"204": {"description": "ok"}}} }} }"#, ); assert_eq!(names(&forge, "a.b.list"), ["x-a"]); - assert_eq!(forge.groups().flat_map(|group| group.methods()).count(), 1); + assert_eq!(forge.namespaces().flat_map(|namespace| namespace.methods()).count(), 1); } #[test] @@ -167,11 +167,11 @@ fn each_method_keeps_its_own_extensions() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "get", "x-a": "get", + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "x-a": "get", "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "list", + "/b": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "responses": {"204": {"description": "ok"}}}}, - "/c": {"post": {"x-forge-group-name": "a", "x-forge-method-name": "create", "x-a": "create", + "/c": {"post": {"x-forge-namespace": "a", "x-forge-method-name": "create", "x-a": "create", "x-b": [], "responses": {"204": {"description": "ok"}}}} } }"#, @@ -190,7 +190,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"x-path-item": 1, "get": {"x-forge-namespace": "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}}}}} }"#, @@ -206,11 +206,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-forge-group-name": "b", "x-forge-method-name": "get", "x-b": 2, + "/b": {"get": {"x-forge-namespace": "b", "x-forge-method-name": "get", "x-b": 2, "responses": {"204": {"description": "ok"}}}} }, "components": {"pathItems": { - "A": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "get", "x-a": 1, + "A": {"get": {"x-forge-namespace": "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 deleted file mode 100644 index 70919dd64..000000000 --- a/tests/parse/groups.rs +++ /dev/null @@ -1,534 +0,0 @@ -use forge::{Forge, Group, Name}; - -use crate::common::{group, method, parse, parse_err}; - -#[test] -fn string_group_name_splits_on_dots() { - let forge = parse( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-forge-group-name": "accounts.members", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - let members = group(&forge, "accounts.members"); - 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-forge-group-name": ["accounts", "members"], "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - let members = group(&forge, "accounts.members"); - assert_eq!(members.parent(), Some(group(&forge, "accounts"))); -} - -#[test] -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-forge-group-name": ["zones.settings"], "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - let groups: Vec = forge.groups().collect(); - let [settings] = groups[..] else { - panic!("want one group, got {groups:?}"); - }; - assert_eq!(settings.name(), &Name::new("zones.settings")); - assert_eq!(settings.parent(), None); -} - -/// Returns a document with one method, `GET /a`, whose group path is written -/// as `group`. -fn method_in(group: &str) -> String { - 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"}}}}}}}}}} - }}"# - ) -} - -/// Returns the one group that has a method, where the group path put it. -#[track_caller] -fn group_with_the_method(forge: &Forge) -> Group<'_> { - let mut groups = forge.groups().filter(|group| group.methods().count() > 0); - let (Some(group), None) = (groups.next(), groups.next()) else { - panic!( - "want one group with methods, got {:?}", - forge.groups().collect::>() - ); - }; - group -} - -/// A string is names separated by dots, and an array is the names, which are -/// not split, so a dot in an array's name stays in it. -#[test] -fn a_dotted_string_is_split_and_a_name_in_an_array_is_not() { - for (written, names) in [ - (r#""a.b""#, vec!["a", "b"]), - (r#""a.b.c""#, vec!["a", "b", "c"]), - (r#"["a", "b"]"#, vec!["a", "b"]), - (r#"["a.b"]"#, vec!["a.b"]), - (r#"["a.b", "c"]"#, vec!["a.b", "c"]), - (r#"["a", "b.c"]"#, vec!["a", "b.c"]), - (r#"["a.b", "c.d"]"#, vec!["a.b", "c.d"]), - ] { - let forge = parse(&method_in(written)); - - let want: Vec = names.iter().map(|name| Name::new(name)).collect(); - assert!( - group_with_the_method(&forge).path().eq(want.iter()), - "{written} is not the path {names:?}" - ); - } -} - -#[test] -fn a_dotted_string_and_an_array_with_that_name_are_different_groups() { - let string = parse(&method_in(r#""zones.settings""#)); - let array = parse(&method_in(r#"["zones.settings"]"#)); - - assert_eq!(group_with_the_method(&string).path().count(), 2); - assert_eq!(group_with_the_method(&array).path().count(), 1); - assert_eq!(string.groups().count(), 2, "zones and zones.settings"); - assert_eq!(array.groups().count(), 1); -} - -#[test] -fn groups_with_equal_names_are_one_group() { - let forge = parse( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": { - "/a": {"get": {"x-forge-group-name": "abuse-reports", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-forge-group-name": ["abuse_reports"], "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}} - } - }"#, - ); - - assert_eq!(forge.groups().count(), 1); - assert_eq!(group(&forge, "abuse_reports").methods().len(), 2); -} - -#[test] -fn subgroup_and_method_can_share_a_name() { - let forge = parse( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": { - "/a": {"delete": {"x-forge-group-name": "members", "x-forge-method-name": "delete", - "responses": {"204": {"description": "ok"}}}}, - "/b": {"post": {"x-forge-group-name": ["members", "delete"], "x-forge-method-name": "bulk", - "responses": {"204": {"description": "ok"}}}} - } - }"#, - ); - - method(&forge, "members.delete"); - method(&forge, "members.delete.bulk"); -} - -#[test] -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-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - assert_eq!(forge.groups().count(), 0); -} - -#[test] -fn vendor_group_and_method_names_do_not_make_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": "get", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - assert_eq!(forge.groups().count(), 0); -} - -#[test] -fn empty_group_name_is_no_group() { - 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] -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-forge-group-name": "a", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - assert_eq!(forge.groups().count(), 0); -} - -#[test] -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-forge-group-name": "a", "x-forge-method-name": "", - "responses": {"204": {"description": "ok"}}}}} - }"#, - ); - - assert_eq!(forge.groups().count(), 0); -} - -#[test] -fn skipped_operation_is_not_a_method() { - let forge = parse( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": { - "get": {"x-forge-group-name": "a", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}, - "post": {"x-forge-group-name": "a", "x-forge-method-name": "create", "x-forge-skip": true, - "responses": {"204": {"description": "ok"}}} - }} - }"#, - ); - - let names: Vec<&Name> = group(&forge, "a").methods().map(|method| method.name()).collect(); - assert_eq!(names, [&Name::new("list")]); -} - -#[test] -fn group_name_without_letters_is_an_error() { - for group in [ - r#"["accounts", "", "members"]"#, - r#""accounts..members""#, - r#"".accounts""#, - r#""accounts.""#, - r#"".""#, - ] { - 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] -fn duplicate_method_in_a_group_is_an_error() { - let err = parse_err( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": { - "/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}} - } - }"#, - ); - - assert!(err.contains("GET /a") && err.contains("GET /b"), "{err}"); -} - -#[test] -fn method_names_are_equal_as_names() { - let err = parse_err( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": { - "/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "list_all", - "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "listAll", - "responses": {"204": {"description": "ok"}}}} - } - }"#, - ); - - assert!(err.contains("GET /a") && err.contains("GET /b"), "{err}"); -} - -#[test] -fn groups_are_depth_first() { - let forge = parse( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": { - "/a": {"get": {"x-forge-group-name": ["a", "b", "c"], "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-forge-group-name": "x", "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}}, - "/c": {"get": {"x-forge-group-name": ["a", "d"], "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}} - } - }"#, - ); - - // Each group is followed by everything below it, whatever the order of - // siblings. - let groups: Vec = forge.groups().collect(); - assert_eq!(groups.len(), 5); - for (i, group) in groups.iter().enumerate() { - let below = group.descendants().count(); - for after in &groups[i + 1..i + 1 + below] { - assert!( - iter_parents(*after).any(|parent| parent == *group), - "{after:?} follows {group:?} but is not below it, in {groups:?}" - ); - } - } -} - -fn iter_parents(group: Group<'_>) -> impl Iterator> { - std::iter::successors(group.parent(), |group| group.parent()) -} - -#[test] -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-forge-group-name": "accounts", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}}, - "x-forge-commands": {"accounts": {"description": "Accounts."}} - }"#, - ); - - assert_eq!(group(&forge, "accounts").description(), "Accounts."); -} - -#[test] -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-forge-group-name": ["accounts", "members"], "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}}, - "x-forge-commands": {"accounts": {"groups": {"members": {"description": "Members."}}}} - }"#, - ); - - assert_eq!(group(&forge, "accounts.members").description(), "Members."); -} - -/// A document with a method in each of `groups`, and `commands` as its -/// `x-forge-commands`. -fn with_commands(groups: &[&str], commands: &str) -> String { - let paths: Vec = groups - .iter() - .enumerate() - .map(|(i, group)| { - let group = group - .split('.') - .map(|name| format!("{name:?}")) - .collect::>() - .join(", "); - format!( - r#""/{i}": {{"get": {{"x-forge-group-name": [{group}], "x-forge-method-name": "list", - "responses": {{"204": {{"description": "ok"}}}}}}}}"# - ) - }) - .collect(); - format!( - r#"{{"openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, - "paths": {{{}}}, "x-forge-commands": {commands}}}"#, - paths.join(", ") - ) -} - -#[test] -fn dotted_group_key_describes_a_subgroup() { - let forge = parse(&with_commands( - &["ai.dynamic-routing.deployments", "ai.dynamic-routing"], - r#"{"ai": {"groups": { - "dynamic-routing": {"description": "Routes."}, - "dynamic-routing.deployments": {"description": "Deployments."}}}}"#, - )); - - assert_eq!(group(&forge, "ai.dynamic_routing").description(), "Routes."); - assert_eq!( - group(&forge, "ai.dynamic_routing.deployments").description(), - "Deployments." - ); -} - -#[test] -fn dotted_root_key_describes_a_subgroup() { - let forge = parse(&with_commands( - &["user.analytics"], - r#"{"user.analytics": {"description": "Analytics."}}"#, - )); - - assert_eq!(group(&forge, "user").description(), ""); - assert_eq!(group(&forge, "user.analytics").description(), "Analytics."); -} - -#[test] -fn dotted_key_can_come_before_its_parents_entry() { - let forge = parse(&with_commands( - &["a.b.c", "a.b", "a"], - r#"{"a.b.c": {"description": "C."}, "a": {"description": "A.", "groups": {"b": {"description": "B."}}}}"#, - )); - - assert_eq!(group(&forge, "a").description(), "A."); - assert_eq!(group(&forge, "a.b").description(), "B."); - assert_eq!(group(&forge, "a.b.c").description(), "C."); -} - -#[test] -fn dotted_and_nested_entries_for_a_group_are_an_error() { - let err = parse_err(&with_commands( - &["a.b"], - r#"{"a": {"groups": {"b": {"description": "One."}}}, "a.b": {"description": "Two."}}"#, - )); - - assert!( - err.contains(r#"two entries in x-forge-commands have the name "b""#), - "{err}" - ); -} - -#[test] -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-forge-group-name": "abuse_reports", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}}, - "x-forge-commands": {"abuse-reports": {"description": "Abuse reports."}} - }"#, - ); - - assert_eq!(group(&forge, "abuse_reports").description(), "Abuse reports."); -} - -#[test] -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-forge-group-name": "accounts", "x-forge-method-name": "list", - "responses": {"204": {"description": "ok"}}}}}, - "x-forge-commands": {"zones": {"description": "Zones."}} - }"#, - ); - - assert_eq!(group(&forge, "accounts").description(), ""); - assert_eq!(forge.groups().count(), 1); -} - -#[test] -fn commands_with_equal_names_are_an_error() { - let err = parse_err( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {}, - "x-forge-commands": {"abuse-reports": {"description": "One."}, "abuse_reports": {"description": "Two."}} - }"#, - ); - - assert!(err.contains("x-forge-commands"), "{err}"); -} - -#[test] -fn groups_are_sorted_by_name() { - let forge = parse( - r#"{ - "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": { - "/a": {"get": {"x-forge-group-name": "zones", "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}}, - "/b": {"get": {"x-forge-group-name": ["accounts", "members"], "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}}, - "/c": {"get": {"x-forge-group-name": "dns", "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}}, - "/d": {"get": {"x-forge-group-name": ["accounts", "billing"], "x-forge-method-name": "get", - "responses": {"204": {"description": "ok"}}}} - } - }"#, - ); - - let paths: Vec = forge - .groups() - .map(|group| group.path().map(Name::snake).collect::>().join(".")) - .collect(); - assert_eq!( - paths, - ["accounts", "accounts.billing", "accounts.members", "dns", "zones"] - ); -} - -#[test] -fn methods_are_sorted_by_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-name": "list", - "responses": {"204": {"description": "ok"}}}, - "post": {"x-forge-group-name": "a", "x-forge-method-name": "create", - "responses": {"204": {"description": "ok"}}}, - "delete": {"x-forge-group-name": "a", "x-forge-method-name": "delete", - "responses": {"204": {"description": "ok"}}} - }} - }"#, - ); - - let names: Vec = group(&forge, "a") - .methods() - .map(|method| method.name().snake()) - .collect(); - assert_eq!(names, ["create", "delete", "list"]); -} diff --git a/tests/parse/headers.rs b/tests/parse/headers.rs index ea395b522..fd860a23d 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [ {"name": "X-Request-ID", "in": "header", "required": true, "description": "Traces the request.", "schema": {"type": "integer", "format": "int64"}}, @@ -43,7 +43,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [{"name": "X-Old", "in": "header", "deprecated": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -95,7 +95,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [ {"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"}}, @@ -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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-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"}}} + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a/{account_id}": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "upload", + "paths": {"/a": {"put": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": ["a", "b"], "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": ["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"]}}} @@ -437,8 +437,8 @@ fn model_used_only_by_a_header_is_a_model() { assert_eq!(models(&forge), ["StorageClass"]); let class = field(headers(&forge, "a.b.get"), "cf-r2-storage-class"); assert_eq!(kind(class.ty()), "Model(StorageClass)"); - let group = model(&forge, "StorageClass").group().unwrap(); - assert_eq!(group.path().collect::>(), [&Name::new("a"), &Name::new("b")]); + let namespace = model(&forge, "StorageClass").namespace().unwrap(); + assert_eq!(namespace.path().collect::>(), [&Name::new("a"), &Name::new("b")]); } #[test] @@ -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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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/main.rs b/tests/parse/main.rs index a7f49e5d8..ab7391c35 100644 --- a/tests/parse/main.rs +++ b/tests/parse/main.rs @@ -6,10 +6,10 @@ mod bodies; mod common; mod document; mod extensions; -mod groups; mod headers; mod methods; mod models; +mod namespaces; mod requests; mod responses; mod types; diff --git a/tests/parse/methods.rs b/tests/parse/methods.rs index ad67eb6f3..30071a7c7 100644 --- a/tests/parse/methods.rs +++ b/tests/parse/methods.rs @@ -1,6 +1,6 @@ use forge::{Availability, HttpMethod, Name}; -use crate::common::{group, method, parse, parse_err}; +use crate::common::{method, namespace, parse, parse_err}; /// Returns a document with one method, `a.get`, at `/a`, whose operation /// also has `fields`, which end with a comma. @@ -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-forge-group-name": "a", "x-forge-method-name": "get", + "x-forge-namespace": "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-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"}}} + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "put": {"x-forge-namespace": "a", "x-forge-method-name": "put", "responses": {"204": {"description": "ok"}}}, + "post": {"x-forge-namespace": "a", "x-forge-method-name": "post", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-namespace": "a", "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}}, + "options": {"x-forge-namespace": "a", "x-forge-method-name": "options", "responses": {"204": {"description": "ok"}}}, + "head": {"x-forge-namespace": "a", "x-forge-method-name": "head", "responses": {"204": {"description": "ok"}}}, + "patch": {"x-forge-namespace": "a", "x-forge-method-name": "patch", "responses": {"204": {"description": "ok"}}}, + "trace": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "search", + "paths": {"/a": {"query": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "copy", + "COPY": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "GET": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "A": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}} }} }"##, @@ -168,19 +168,19 @@ 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-forge-group-name": "x", "x-forge-method-name": "get", + "x-paths-extension": {"get": {"x-forge-namespace": "x", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}, "/a": { "x-forge-commands": {"a": {"description": "A."}}, - "get": {"x-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}} } } }"#, ); - assert_eq!(forge.groups().count(), 1); - assert_eq!(group(&forge, "a").methods().len(), 1); + assert_eq!(forge.namespaces().count(), 1); + assert_eq!(namespace(&forge, "a").methods().len(), 1); } #[test] @@ -189,15 +189,15 @@ 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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, - "additionalOperations": {"GET": {"x-forge-group-name": "a", "x-forge-method-name": "shadowed", + "additionalOperations": {"GET": {"x-forge-namespace": "a", "x-forge-method-name": "shadowed", "responses": {"204": {"description": "ok"}}}} }} }"#, ); - let names: Vec<&Name> = group(&forge, "a").methods().map(|method| method.name()).collect(); + let names: Vec<&Name> = namespace(&forge, "a").methods().map(|method| method.name()).collect(); assert_eq!(names, [&Name::new("get")]); } @@ -206,12 +206,12 @@ fn webhooks_are_not_methods() { let forge = parse( r#"{ "openapi": "3.1.0", "info": {"title": "t", "version": "1"}, - "webhooks": {"newPet": {"post": {"x-forge-group-name": "pets", "x-forge-method-name": "created", + "webhooks": {"newPet": {"post": {"x-forge-namespace": "pets", "x-forge-method-name": "created", "responses": {"204": {"description": "ok"}}}}} }"#, ); - assert_eq!(forge.groups().count(), 0); + assert_eq!(forge.namespaces().count(), 0); } #[test] @@ -219,16 +219,16 @@ fn callbacks_are_not_methods() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/subscribe": {"post": {"x-forge-group-name": "a", "x-forge-method-name": "subscribe", + "paths": {"/subscribe": {"post": {"x-forge-namespace": "a", "x-forge-method-name": "subscribe", "callbacks": {"event": {"{$request.query.url}": {"post": { - "x-forge-group-name": "a", "x-forge-method-name": "event", + "x-forge-namespace": "a", "x-forge-method-name": "event", "responses": {"204": {"description": "ok"}} }}}}, "responses": {"204": {"description": "ok"}}}}} }"#, ); - assert_eq!(group(&forge, "a").methods().len(), 1); + assert_eq!(namespace(&forge, "a").methods().len(), 1); } #[test] @@ -331,13 +331,13 @@ fn availability_and_deprecation_are_apart() { r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": {"/a": { - "get": {"x-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "deprecated": true, "x-forge-availability": "generally-available", "responses": {"204": {"description": "ok"}}}, - "put": {"x-forge-group-name": "a", "x-forge-method-name": "put", + "put": {"x-forge-namespace": "a", "x-forge-method-name": "put", "x-forge-availability": "deprecated", "responses": {"204": {"description": "ok"}}}, - "post": {"x-forge-group-name": "a", "x-forge-method-name": "post", + "post": {"x-forge-namespace": "a", "x-forge-method-name": "post", "deprecated": true, "responses": {"204": {"description": "ok"}}} }} }"#, @@ -359,10 +359,10 @@ 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", + "paths": {"/a": {"get": {"x-forge-namespace": ["a"], "x-forge-method": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); - assert_eq!(forge.groups().count(), 0); + assert_eq!(forge.namespaces().count(), 0); } diff --git a/tests/parse/models.rs b/tests/parse/models.rs index cd8337afa..56d7982ac 100644 --- a/tests/parse/models.rs +++ b/tests/parse/models.rs @@ -1,11 +1,11 @@ use forge::{Name, Scalar}; use crate::common::{ - field, group, method, model, model_of, models, object, parse, responding_with_schemas, response_type, scalar, + field, method, model, model_of, models, namespace, object, parse, responding_with_schemas, response_type, scalar, }; #[test] -fn response_model_is_placed_in_its_group() { +fn response_model_is_placed_in_its_namespace() { let forge = parse( r##"{ "openapi": "3.0.3", @@ -13,7 +13,7 @@ fn response_model_is_placed_in_its_group() { "paths": { "/zones": { "get": { - "x-forge-group-name": "zones", + "x-forge-namespace": "zones", "x-forge-method-name": "list", "responses": { "200": { @@ -34,54 +34,54 @@ fn response_model_is_placed_in_its_group() { }"##, ); - assert_eq!(model(&forge, "Zone").group(), Some(group(&forge, "zones"))); + assert_eq!(model(&forge, "Zone").namespace(), Some(namespace(&forge, "zones"))); } #[test] -fn model_used_in_sibling_groups_is_in_their_parent() { +fn model_used_in_sibling_namespaces_is_in_their_parent() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/c": {"get": {"x-forge-group-name": ["a", "b", "c"], "x-forge-method-name": "get", + "/c": {"get": {"x-forge-namespace": ["a", "b", "c"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/d": {"get": {"x-forge-group-name": ["a", "b", "d"], "x-forge-method-name": "get", + "/d": {"get": {"x-forge-namespace": ["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"}}}}} }"##, ); - assert_eq!(model(&forge, "M").group(), Some(group(&forge, "a.b"))); + assert_eq!(model(&forge, "M").namespace(), Some(namespace(&forge, "a.b"))); } #[test] -fn model_used_in_a_group_and_its_subgroup_is_in_the_group() { +fn model_used_in_a_namespace_and_its_subnamespace_is_in_the_namespace() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "get", + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/c": {"get": {"x-forge-group-name": ["a", "b", "c"], "x-forge-method-name": "get", + "/c": {"get": {"x-forge-namespace": ["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"}}}}} }"##, ); - assert_eq!(model(&forge, "M").group(), Some(group(&forge, "a"))); + assert_eq!(model(&forge, "M").namespace(), Some(namespace(&forge, "a"))); } #[test] -fn model_used_in_top_level_groups_is_at_the_top_level() { +fn model_used_in_top_level_namespaces_is_at_the_top_level() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "get", + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/x": {"get": {"x-forge-group-name": "x", "x-forge-method-name": "get", + "/x": {"get": {"x-forge-namespace": "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"}}}}} @@ -89,7 +89,7 @@ fn model_used_in_top_level_groups_is_at_the_top_level() { ); let m = model(&forge, "M"); - assert_eq!(m.group(), None); + assert_eq!(m.namespace(), None); assert_eq!(forge.models().collect::>(), [m]); } @@ -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-forge-group-name": ["a", "b"], "x-forge-method-name": "get", + "/b": {"get": {"x-forge-namespace": ["a", "b"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Outer"}}}}}}}, - "/c": {"get": {"x-forge-group-name": ["a", "c"], "x-forge-method-name": "get", + "/c": {"get": {"x-forge-namespace": ["a", "c"], "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Inner"}}}}}}} }, "components": {"schemas": { @@ -111,8 +111,8 @@ fn model_used_through_another_model_is_placed_by_every_use() { }"##, ); - assert_eq!(model(&forge, "Outer").group(), Some(group(&forge, "a.b"))); - assert_eq!(model(&forge, "Inner").group(), Some(group(&forge, "a"))); + assert_eq!(model(&forge, "Outer").namespace(), Some(namespace(&forge, "a.b"))); + assert_eq!(model(&forge, "Inner").namespace(), Some(namespace(&forge, "a"))); } #[test] @@ -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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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"}}}}}}}}, @@ -132,9 +132,9 @@ fn params_bodies_and_errors_use_models() { }"##, ); - let a = group(&forge, "a"); + let a = namespace(&forge, "a"); for name in ["Filter", "Input", "Failure"] { - assert_eq!(model(&forge, name).group(), Some(a), "{name}"); + assert_eq!(model(&forge, name).namespace(), Some(a), "{name}"); } } @@ -144,17 +144,17 @@ fn schema_no_method_uses_is_not_a_model() { r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, "paths": { - "/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "get", + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Used"}}}}}}}, - "/skipped": {"get": {"x-forge-group-name": ["a"], "x-forge-method-name": "skipped", "x-forge-skip": true, + "/skipped": {"get": {"x-forge-namespace": ["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"}}}}}}} + "/unnamespaceed": {"get": {"x-forge-method-name": "unnamespaceed", + "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Unnamespaceed"}}}}}}} }, "components": {"schemas": { "Used": {"type": "string"}, "Skipped": {"type": "string"}, - "Ungrouped": {"type": "string"}, + "Unnamespaceed": {"type": "string"}, "Unused": {"type": "string"} }} }"##, @@ -172,7 +172,7 @@ fn model_can_refer_to_itself() { let node = model(&forge, "Node"); assert_eq!(model_of(field(object(node.ty()), "next").ty()), node); - assert_eq!(node.group(), Some(group(&forge, "a"))); + assert_eq!(node.namespace(), Some(namespace(&forge, "a"))); } #[test] @@ -234,7 +234,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}}} @@ -258,7 +258,7 @@ fn model_that_is_a_ref_is_the_other_model() { let base = model_of(model(&forge, "Alias").ty()); assert_eq!(base.schema_name(), "Base"); assert_eq!(scalar(base.ty()), Scalar::String); - assert_eq!(base.group(), Some(group(&forge, "a"))); + assert_eq!(base.namespace(), Some(namespace(&forge, "a"))); } #[test] @@ -266,7 +266,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-forge-group-name": ["a", "files"], "x-forge-method-name": "upload", + "paths": {"/a": {"post": {"x-forge-namespace": ["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": { @@ -277,9 +277,9 @@ fn schema_used_only_by_content_that_is_not_json_is_a_model() { ); assert_eq!(models(&forge), ["Report", "Upload"]); - let files = group(&forge, "a.files"); - assert_eq!(model(&forge, "Upload").group(), Some(files)); - assert_eq!(model(&forge, "Report").group(), Some(files)); + let files = namespace(&forge, "a.files"); + assert_eq!(model(&forge, "Upload").namespace(), Some(files)); + assert_eq!(model(&forge, "Report").namespace(), Some(files)); let upload = method(&forge, "a.files.upload") .body() .unwrap() @@ -297,9 +297,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-forge-group-name": "a", "x-forge-method-name": "get", + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/M"}}}}}}}, - "/b": {"post": {"x-forge-group-name": "b", "x-forge-method-name": "upload", + "/b": {"post": {"x-forge-namespace": "b", "x-forge-method-name": "upload", "requestBody": {"content": {"multipart/form-data": {"schema": { "type": "object", "properties": {"meta": {"$ref": "#/components/schemas/M"}} }}}}, @@ -309,7 +309,7 @@ fn use_by_content_that_is_not_json_places_a_model() { }"##, ); - assert_eq!(model(&forge, "M").group(), None); + assert_eq!(model(&forge, "M").namespace(), None); } #[test] @@ -317,7 +317,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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/namespaces.rs b/tests/parse/namespaces.rs new file mode 100644 index 000000000..4a459070c --- /dev/null +++ b/tests/parse/namespaces.rs @@ -0,0 +1,636 @@ +use std::borrow::Cow; + +use forge::{Err, File, Forge, Name, Namespace, Plugin, run}; + +use crate::common::{method, namespace, parse, parse_err}; + +#[test] +fn string_namespace_name_splits_on_dots() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": "accounts.members", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + let members = namespace(&forge, "accounts.members"); + assert_eq!(members.parent(), Some(namespace(&forge, "accounts"))); +} + +#[test] +fn deprecated_group_name_is_a_namespace_alias() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-group-name": "accounts.members", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + let members = namespace(&forge, "accounts.members"); + assert_eq!(members.parent(), Some(namespace(&forge, "accounts"))); +} + +#[test] +#[expect(deprecated, reason = "this test exercises the deprecated Rust aliases")] +fn deprecated_rust_aliases_forward_to_namespaces() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": "accounts", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + let groups: Vec> = forge.groups().collect(); + assert_eq!(groups, forge.namespaces().collect::>()); + assert_eq!(groups[0].methods().next().unwrap().group(), groups[0]); +} + +struct LegacyPlugin; + +impl Plugin for LegacyPlugin { + type File = LegacyFile; + + fn generate_group(&self, namespace: Namespace<'_>) -> Result, Err> { + Ok(std::iter::once(LegacyFile(namespace.name().kebab()))) + } +} + +struct LegacyFile(String); + +impl File for LegacyFile { + fn path(&self) -> &str { + &self.0 + } + + fn content(&self) -> Result, Err> { + Ok(Cow::Borrowed(b"legacy")) + } +} + +#[test] +fn run_calls_a_deprecated_generate_group_implementation() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": "accounts", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + let files = run(&forge, &LegacyPlugin).unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].path(), "accounts"); +} + +#[test] +fn namespace_key_wins_over_deprecated_group_name_in_either_order() { + for definition in [ + r#""x-forge-group-name": "old", "x-forge-namespace": "new""#, + r#""x-forge-namespace": "new", "x-forge-group-name": "old""#, + ] { + let forge = parse(&format!( + r#"{{ + "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{{definition}, "x-forge-method-name": "list", + "responses": {{"204": {{"description": "ok"}}}}}}}}}} + }}"# + )); + + assert_eq!(forge.namespaces().count(), 1); + namespace(&forge, "new"); + } +} + +#[test] +fn namespace_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-namespace": 1, "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + assert!(err.contains("a string or an array of strings"), "{err}"); +} + +#[test] +fn array_namespace_name_has_a_namespace_per_element() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": ["accounts", "members"], "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + let members = namespace(&forge, "accounts.members"); + assert_eq!(members.parent(), Some(namespace(&forge, "accounts"))); +} + +#[test] +fn array_element_with_a_dot_is_one_namespace() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": ["zones.settings"], "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + let namespaces: Vec = forge.namespaces().collect(); + let [settings] = namespaces[..] else { + panic!("want one namespace, got {namespaces:?}"); + }; + assert_eq!(settings.name(), &Name::new("zones.settings")); + assert_eq!(settings.parent(), None); +} + +/// Returns a document with one method, `GET /a`, whose namespace path is written +/// as `namespace`. +fn method_in(namespace: &str) -> String { + format!( + r#"{{ + "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{"x-forge-namespace": {namespace}, "x-forge-method-name": "get", + "responses": {{"204": {{"description": "ok"}}}}}}}}}} + }}"# + ) +} + +/// Returns the one namespace that has a method, where the namespace path put it. +#[track_caller] +fn namespace_with_the_method(forge: &Forge) -> Namespace<'_> { + let mut namespaces = forge.namespaces().filter(|namespace| namespace.methods().count() > 0); + let (Some(namespace), None) = (namespaces.next(), namespaces.next()) else { + panic!( + "want one namespace with methods, got {:?}", + forge.namespaces().collect::>() + ); + }; + namespace +} + +/// A string is names separated by dots, and an array is the names, which are +/// not split, so a dot in an array's name stays in it. +#[test] +fn a_dotted_string_is_split_and_a_name_in_an_array_is_not() { + for (written, names) in [ + (r#""a.b""#, vec!["a", "b"]), + (r#""a.b.c""#, vec!["a", "b", "c"]), + (r#"["a", "b"]"#, vec!["a", "b"]), + (r#"["a.b"]"#, vec!["a.b"]), + (r#"["a.b", "c"]"#, vec!["a.b", "c"]), + (r#"["a", "b.c"]"#, vec!["a", "b.c"]), + (r#"["a.b", "c.d"]"#, vec!["a.b", "c.d"]), + ] { + let forge = parse(&method_in(written)); + + let want: Vec = names.iter().map(|name| Name::new(name)).collect(); + assert!( + namespace_with_the_method(&forge).path().eq(want.iter()), + "{written} is not the path {names:?}" + ); + } +} + +#[test] +fn a_dotted_string_and_an_array_with_that_name_are_different_namespaces() { + let string = parse(&method_in(r#""zones.settings""#)); + let array = parse(&method_in(r#"["zones.settings"]"#)); + + assert_eq!(namespace_with_the_method(&string).path().count(), 2); + assert_eq!(namespace_with_the_method(&array).path().count(), 1); + assert_eq!(string.namespaces().count(), 2, "zones and zones.settings"); + assert_eq!(array.namespaces().count(), 1); +} + +#[test] +fn namespaces_with_equal_names_are_one_namespace() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": { + "/a": {"get": {"x-forge-namespace": "abuse-reports", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}, + "/b": {"get": {"x-forge-namespace": ["abuse_reports"], "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}} + } + }"#, + ); + + assert_eq!(forge.namespaces().count(), 1); + assert_eq!(namespace(&forge, "abuse_reports").methods().len(), 2); +} + +#[test] +fn subnamespace_and_method_can_share_a_name() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": { + "/a": {"delete": {"x-forge-namespace": "members", "x-forge-method-name": "delete", + "responses": {"204": {"description": "ok"}}}}, + "/b": {"post": {"x-forge-namespace": ["members", "delete"], "x-forge-method-name": "bulk", + "responses": {"204": {"description": "ok"}}}} + } + }"#, + ); + + method(&forge, "members.delete"); + method(&forge, "members.delete.bulk"); +} + +#[test] +fn operation_without_a_namespace_is_not_a_method() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + assert_eq!(forge.namespaces().count(), 0); +} + +#[test] +fn vendor_namespace_and_method_names_do_not_make_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": "get", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + assert_eq!(forge.namespaces().count(), 0); +} + +#[test] +fn empty_namespace_name_is_no_namespace() { + for namespace in [r#""""#, "[]"] { + let forge = parse(&format!( + r#"{{ + "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{"x-forge-namespace": {namespace}, "x-forge-method-name": "get", + "responses": {{"204": {{"description": "ok"}}}}}}}}}} + }}"# + )); + + assert_eq!(forge.namespaces().count(), 0, "{namespace}"); + } +} + +#[test] +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-forge-namespace": "a", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + assert_eq!(forge.namespaces().count(), 0); +} + +#[test] +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-forge-namespace": "a", "x-forge-method-name": "", + "responses": {"204": {"description": "ok"}}}}} + }"#, + ); + + assert_eq!(forge.namespaces().count(), 0); +} + +#[test] +fn skipped_operation_is_not_a_method() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": { + "get": {"x-forge-namespace": "a", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}, + "post": {"x-forge-namespace": "a", "x-forge-method-name": "create", "x-forge-skip": true, + "responses": {"204": {"description": "ok"}}} + }} + }"#, + ); + + let names: Vec<&Name> = namespace(&forge, "a").methods().map(|method| method.name()).collect(); + assert_eq!(names, [&Name::new("list")]); +} + +#[test] +fn namespace_name_without_letters_is_an_error() { + for namespace in [ + r#"["accounts", "", "members"]"#, + r#""accounts..members""#, + r#"".accounts""#, + r#""accounts.""#, + r#"".""#, + ] { + let err = parse_err(&format!( + r#"{{ + "openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{"/a": {{"get": {{"x-forge-namespace": {namespace}, "x-forge-method-name": "list", + "responses": {{"204": {{"description": "ok"}}}}}}}}}} + }}"# + )); + + assert!( + err.contains("GET /a: x-forge-namespace has a namespace name"), + "{namespace}: {err}" + ); + } +} + +#[test] +fn duplicate_method_in_a_namespace_is_an_error() { + let err = parse_err( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": { + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}, + "/b": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}} + } + }"#, + ); + + assert!(err.contains("GET /a") && err.contains("GET /b"), "{err}"); +} + +#[test] +fn method_names_are_equal_as_names() { + let err = parse_err( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": { + "/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list_all", + "responses": {"204": {"description": "ok"}}}}, + "/b": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "listAll", + "responses": {"204": {"description": "ok"}}}} + } + }"#, + ); + + assert!(err.contains("GET /a") && err.contains("GET /b"), "{err}"); +} + +#[test] +fn namespaces_are_depth_first() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": { + "/a": {"get": {"x-forge-namespace": ["a", "b", "c"], "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}}, + "/b": {"get": {"x-forge-namespace": "x", "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}}, + "/c": {"get": {"x-forge-namespace": ["a", "d"], "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}} + } + }"#, + ); + + // Each namespace is followed by everything below it, whatever the order of + // siblings. + let namespaces: Vec = forge.namespaces().collect(); + assert_eq!(namespaces.len(), 5); + for (i, namespace) in namespaces.iter().enumerate() { + let below = namespace.descendants().count(); + for after in &namespaces[i + 1..i + 1 + below] { + assert!( + iter_parents(*after).any(|parent| parent == *namespace), + "{after:?} follows {namespace:?} but is not below it, in {namespaces:?}" + ); + } + } +} + +fn iter_parents(namespace: Namespace<'_>) -> impl Iterator> { + std::iter::successors(namespace.parent(), |namespace| namespace.parent()) +} + +#[test] +fn namespace_description_comes_from_x_forge_commands() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": "accounts", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}}, + "x-forge-commands": {"accounts": {"description": "Accounts."}} + }"#, + ); + + assert_eq!(namespace(&forge, "accounts").description(), "Accounts."); +} + +#[test] +fn subnamespace_description_is_in_its_parents_command() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": ["accounts", "members"], "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}}, + "x-forge-commands": {"accounts": {"namespaces": {"members": {"description": "Members."}}}} + }"#, + ); + + assert_eq!(namespace(&forge, "accounts.members").description(), "Members."); +} + +#[test] +fn deprecated_groups_key_describes_a_subnamespace() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": ["accounts", "members"], "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}}, + "x-forge-commands": {"accounts": {"groups": {"members": {"description": "Members."}}}} + }"#, + ); + + assert_eq!(namespace(&forge, "accounts.members").description(), "Members."); +} + +/// A document with a method in each of `namespaces`, and `commands` as its +/// `x-forge-commands`. +fn with_commands(namespaces: &[&str], commands: &str) -> String { + let paths: Vec = namespaces + .iter() + .enumerate() + .map(|(i, namespace)| { + let namespace = namespace + .split('.') + .map(|name| format!("{name:?}")) + .collect::>() + .join(", "); + format!( + r#""/{i}": {{"get": {{"x-forge-namespace": [{namespace}], "x-forge-method-name": "list", + "responses": {{"204": {{"description": "ok"}}}}}}}}"# + ) + }) + .collect(); + format!( + r#"{{"openapi": "3.0.3", "info": {{"title": "t", "version": "1"}}, + "paths": {{{}}}, "x-forge-commands": {commands}}}"#, + paths.join(", ") + ) +} + +#[test] +fn dotted_namespace_key_describes_a_subnamespace() { + let forge = parse(&with_commands( + &["ai.dynamic-routing.deployments", "ai.dynamic-routing"], + r#"{"ai": {"namespaces": { + "dynamic-routing": {"description": "Routes."}, + "dynamic-routing.deployments": {"description": "Deployments."}}}}"#, + )); + + assert_eq!(namespace(&forge, "ai.dynamic_routing").description(), "Routes."); + assert_eq!( + namespace(&forge, "ai.dynamic_routing.deployments").description(), + "Deployments." + ); +} + +#[test] +fn dotted_root_key_describes_a_subnamespace() { + let forge = parse(&with_commands( + &["user.analytics"], + r#"{"user.analytics": {"description": "Analytics."}}"#, + )); + + assert_eq!(namespace(&forge, "user").description(), ""); + assert_eq!(namespace(&forge, "user.analytics").description(), "Analytics."); +} + +#[test] +fn dotted_key_can_come_before_its_parents_entry() { + let forge = parse(&with_commands( + &["a.b.c", "a.b", "a"], + r#"{"a.b.c": {"description": "C."}, "a": {"description": "A.", "namespaces": {"b": {"description": "B."}}}}"#, + )); + + assert_eq!(namespace(&forge, "a").description(), "A."); + assert_eq!(namespace(&forge, "a.b").description(), "B."); + assert_eq!(namespace(&forge, "a.b.c").description(), "C."); +} + +#[test] +fn dotted_and_nested_entries_for_a_namespace_are_an_error() { + let err = parse_err(&with_commands( + &["a.b"], + r#"{"a": {"namespaces": {"b": {"description": "One."}}}, "a.b": {"description": "Two."}}"#, + )); + + assert!( + err.contains(r#"two entries in x-forge-commands have the name "b""#), + "{err}" + ); +} + +#[test] +fn command_matches_a_namespace_with_an_equal_name() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": "abuse_reports", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}}, + "x-forge-commands": {"abuse-reports": {"description": "Abuse reports."}} + }"#, + ); + + assert_eq!(namespace(&forge, "abuse_reports").description(), "Abuse reports."); +} + +#[test] +fn namespace_without_a_command_has_no_description() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": {"get": {"x-forge-namespace": "accounts", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}}}, + "x-forge-commands": {"zones": {"description": "Zones."}} + }"#, + ); + + assert_eq!(namespace(&forge, "accounts").description(), ""); + assert_eq!(forge.namespaces().count(), 1); +} + +#[test] +fn commands_with_equal_names_are_an_error() { + let err = parse_err( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {}, + "x-forge-commands": {"abuse-reports": {"description": "One."}, "abuse_reports": {"description": "Two."}} + }"#, + ); + + assert!(err.contains("x-forge-commands"), "{err}"); +} + +#[test] +fn namespaces_are_sorted_by_name() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": { + "/a": {"get": {"x-forge-namespace": "zones", "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}}, + "/b": {"get": {"x-forge-namespace": ["accounts", "members"], "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}}, + "/c": {"get": {"x-forge-namespace": "dns", "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}}, + "/d": {"get": {"x-forge-namespace": ["accounts", "billing"], "x-forge-method-name": "get", + "responses": {"204": {"description": "ok"}}}} + } + }"#, + ); + + let paths: Vec = forge + .namespaces() + .map(|namespace| namespace.path().map(Name::snake).collect::>().join(".")) + .collect(); + assert_eq!( + paths, + ["accounts", "accounts.billing", "accounts.members", "dns", "zones"] + ); +} + +#[test] +fn methods_are_sorted_by_name() { + let forge = parse( + r#"{ + "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, + "paths": {"/a": { + "get": {"x-forge-namespace": "a", "x-forge-method-name": "list", + "responses": {"204": {"description": "ok"}}}, + "post": {"x-forge-namespace": "a", "x-forge-method-name": "create", + "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-namespace": "a", "x-forge-method-name": "delete", + "responses": {"204": {"description": "ok"}}} + }} + }"#, + ); + + let names: Vec = namespace(&forge, "a") + .methods() + .map(|method| method.name().snake()) + .collect(); + assert_eq!(names, ["create", "delete", "list"]); +} diff --git a/tests/parse/requests.rs b/tests/parse/requests.rs index f75383bf5..8c726c92a 100644 --- a/tests/parse/requests.rs +++ b/tests/parse/requests.rs @@ -13,7 +13,7 @@ fn path_param_is_a_required_request_field() { "paths": { "/zones/{zone_id}": { "get": { - "x-forge-group-name": "zones", + "x-forge-namespace": "zones", "x-forge-method-name": "get", "parameters": [ { "name": "zone_id", "in": "path", "required": true, "schema": { "type": "string" } } @@ -44,7 +44,7 @@ fn vendor_parameter_name_is_not_read() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [{"name": "wire_name", "in": "query", "x-fern-parameter-name": "renamed", "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} @@ -61,7 +61,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -78,7 +78,7 @@ fn query_param_can_be_required() { let forge = parse( r#"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -93,7 +93,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}}}}} @@ -110,7 +110,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [ {"name": "X-Trace", "in": "header", "schema": {"type": "string"}}, {"name": "session", "in": "cookie", "schema": {"type": "string"}} @@ -128,7 +128,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "q", "in": "query", "x-forge-parameter-name": "query", "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -143,7 +143,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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."}} @@ -162,7 +162,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}} @@ -182,7 +182,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"$ref": "#/components/schemas/Id"}}], "responses": {"204": {"description": "ok"}}}}}, "components": {"schemas": { @@ -201,7 +201,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}} @@ -221,7 +221,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}}}}}, @@ -238,7 +238,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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_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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "region", "in": "query", "schema": {"allOf": [{"$ref": "#/components/schemas/Region"}, {"default": "us"}]}}], "responses": {"204": {"description": "ok"}}}}}, @@ -272,7 +272,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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_description_is_ignored_in_3_0() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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.", @@ -306,7 +306,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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.", @@ -323,7 +323,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-namespace": "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}}, @@ -348,7 +348,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-sensitive": true}} }}}}], @@ -366,7 +366,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [ {"name": "token", "in": "query", "schema": {"type": "string", "x-forge-sensitive": true}}, {"name": "page", "in": "query", "schema": {"type": "integer"}} @@ -385,7 +385,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "token", "in": "query", "schema": {"$ref": "#/components/schemas/Token"}}], "responses": {"204": {"description": "ok"}}}}}, "components": {"schemas": { @@ -403,7 +403,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "token", "in": "query", "schema": {"$ref": "#/components/schemas/Token", "x-forge-sensitive": true}}], "responses": {"204": {"description": "ok"}}}}}, @@ -419,7 +419,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "token", "in": "query", "x-forge-sensitive": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -433,7 +433,7 @@ fn param_ref_is_followed() { let forge = parse( r##"{ "openapi": "3.0.3", "info": {"title": "t", "version": "1"}, - "paths": {"/a": {"get": {"x-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}}}} @@ -452,8 +452,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-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"}}} + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-namespace": "a", "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}} }} }"#, ); @@ -472,7 +472,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-forge-group-name": "a", "x-forge-method-name": "list", + "get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}} }} @@ -489,7 +489,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -508,7 +508,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-forge-group-name": "a", "x-forge-method-name": "get", + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [ {"name": "op_query", "in": "query", "schema": {"type": "string"}}, {"name": "first", "in": "path", "required": true, "schema": {"type": "string"}}, @@ -528,7 +528,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}} }}}}], @@ -549,7 +549,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}}} }"#, ); @@ -562,7 +562,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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "a", "x-forge-method-name": "create", "requestBody": {"content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "responses": {"204": {"description": "ok"}}}}} }"#, @@ -576,7 +576,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"}} @@ -599,7 +599,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"}}}}}, @@ -621,7 +621,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"}] @@ -648,7 +648,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"}}}}} @@ -668,7 +668,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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-forge-sensitive": true @@ -687,7 +687,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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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": { @@ -707,7 +707,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"} @@ -725,7 +725,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"}}}}} @@ -741,7 +741,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"}} @@ -759,7 +759,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{zone_id}": {"put": {"x-forge-namespace": "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"}} @@ -779,8 +779,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-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"}}} + "get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"204": {"description": "ok"}}}, + "delete": {"x-forge-namespace": "a", "x-forge-method-name": "delete", "responses": {"204": {"description": "ok"}}} }} }"#, ); @@ -796,7 +796,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-forge-group-name": "a", "x-forge-method-name": "upload", + "paths": {"/a/{id}": {"post": {"x-forge-namespace": "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"}} @@ -815,7 +815,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-forge-group-name": "a", "x-forge-method-name": "upload", + "paths": {"/a": {"post": {"x-forge-namespace": "a", "x-forge-method-name": "upload", "requestBody": {"content": {"multipart/form-data": {"schema": { "type": "object", "properties": {"file": {"type": "string", "format": "binary"}} }}}}, @@ -833,7 +833,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-namespace": "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"}} @@ -850,7 +850,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "page", "in": "query", "deprecated": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -864,7 +864,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a/{id}": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [{"name": "id", "in": "path", "required": true, "deprecated": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -878,7 +878,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "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"}] @@ -905,7 +905,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "filter", "in": "query", "content": {"application/json": {"schema": { "type": "object", "properties": {"status": {"type": "string"}} }}}}], @@ -929,7 +929,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {{"/a": {{"get": {{"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{{"name": "filter", "in": "query", "style": "{style}", "schema": {schema}}}], "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# @@ -944,7 +944,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "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"}}}} @@ -963,7 +963,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-forge-group-name": "a", "x-forge-method-name": "list", + "get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "filter", "in": "querystring", "content": {"application/x-www-form-urlencoded": {"schema": {"type": "object"}}}}], "responses": {"204": {"description": "ok"}}} }} @@ -980,10 +980,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-forge-group-name": "a", "x-forge-method-name": "replace", + "put": {"x-forge-namespace": "a", "x-forge-method-name": "replace", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "responses": {"204": {"description": "ok"}}}, - "patch": {"x-forge-group-name": "a", "x-forge-method-name": "append", + "patch": {"x-forge-namespace": "a", "x-forge-method-name": "append", "requestBody": {"content": {"application/json": {"schema": {"type": "array", "items": {"type": "string"}}}}}, "responses": {"204": {"description": "ok"}}} }} @@ -1000,7 +1000,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a/{zone_id}": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [ {"name": "zone_id", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "zoneId", "in": "query", "schema": {"type": "string"}} @@ -1018,7 +1018,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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {{"/a/{{id}}": {{"get": {{"x-forge-namespace": "a", "x-forge-method-name": "get", "parameters": [{{"name": "id", "in": "path", "required": true, "style": "{style}", "schema": {{"type": "string"}}}}], "responses": {{"204": {{"description": "ok"}}}}}}}}}} }}"# @@ -1033,7 +1033,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [ {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}}, {"name": "page", "in": "query", "schema": {"type": "integer"}} @@ -1052,7 +1052,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a/{order}": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [ {"name": "order", "in": "path", "required": true, "schema": {"$ref": "#/components/schemas/Order"}}, {"$ref": "#/components/parameters/Direction"} @@ -1079,7 +1079,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "per_page", "in": "query", "schema": {"$ref": "#/components/schemas/PerPage", "default": 50}}], "responses": {"204": {"description": "ok"}}}}}, @@ -1096,7 +1096,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-forge-group-name": "a", "x-forge-method-name": "list", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "list", "parameters": [{"name": "per_page", "in": "query", "default": 20, "schema": {"type": "integer"}}], "responses": {"204": {"description": "ok"}}}}} }"#, @@ -1113,7 +1113,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-forge-group-name": "a", "x-forge-method-name": "update", + "paths": {"/a/{id}": {"put": {"x-forge-namespace": "a", "x-forge-method-name": "update", "requestBody": {"content": {"application/json": {"schema": { "type": "array", "items": {"type": "string"}, "default": ["x"] }}}}, @@ -1131,7 +1131,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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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 7b1de934a..041b7114f 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-forge-group-name": "a", "x-forge-method-name": "create", + "paths": {"/a": {"post": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "delete", + "paths": {"/a": {"delete": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "download", + "paths": {"/a": {"get": {"x-forge-namespace": "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 9d56960f7..20a06f522 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 85ec33c22..2eaa2acdc 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 d9f154ae0..bd98fe450 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 87fadcbab..f22ba8750 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 d6fac5b38..7d917d286 100644 --- a/tests/parse/types/objects.rs +++ b/tests/parse/types/objects.rs @@ -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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"id": {"type": "string"}, "gone": false} }}}}}}}} diff --git a/tests/parse/types/refs.rs b/tests/parse/types/refs.rs index b3170080f..88796b84f 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"id": {"$ref": "#/components/schemas/Id", "readOnly": false}} }}}}}}}}, @@ -386,7 +386,7 @@ 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": {"token": {"$ref": "#/components/schemas/Token", "x-forge-sensitive": true}} }}}}}}}}, @@ -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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 b54d8d670..48cb2210b 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 897d7c28f..e23f0f047 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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "a", "x-forge-method-name": "get", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": ["string", "integer"] }}}}}}}} @@ -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-forge-group-name": "a", "x-forge-method-name": "get", + "paths": {"/a": {"get": {"x-forge-namespace": "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 18bedd8b9..ebb4463bf 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -30,7 +30,7 @@ mod bin { pub mod ts; } -/// A document with nested groups, path and query parameters, bodies, +/// A document with nested namespaces, path and query parameters, bodies, /// errors, and models that are enums, unions with a discriminator, /// intersections, maps and nullable, and that refer to each other in a /// cycle. @@ -41,7 +41,7 @@ const SPEC: &str = r##"{ "paths": { "/pets": { "get": { - "x-forge-group-name": ["pets"], "x-forge-method-name": "list", "summary": "Lists pets.", + "x-forge-namespace": ["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-forge-group-name": ["pets"], "x-forge-method-name": "create", "deprecated": true, + "x-forge-namespace": ["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-forge-group-name": ["pets"], "x-forge-method-name": "get", + "x-forge-namespace": ["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-forge-group-name": ["pets", "photos"], "x-forge-method-name": "upload", + "x-forge-namespace": ["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-forge-group-name": ["owners"], "x-forge-method-name": "list", + "x-forge-namespace": ["owners"], "x-forge-method-name": "list", "responses": {"200": {"description": "ok", "content": {"application/json": {"schema": { "type": "object", "properties": { @@ -128,7 +128,7 @@ const SPEC: &str = r##"{ /// doesn't use: header parameters, availability, hidden methods, /// confirmations, aliases, extensions of every JSON type, read-only, /// sensitive and defaulted fields, limits on arrays, binary content, -/// descriptions of content, unions that are not exclusive, and groups +/// descriptions of content, unions that are not exclusive, and namespaces /// described with dotted keys. const FEATURES: &str = r##"{ "openapi": "3.1.0", @@ -138,7 +138,7 @@ const FEATURES: &str = r##"{ "/pets/{id}": { "parameters": [{"name": "X-Trace", "in": "header", "schema": {"type": "string"}}], "get": { - "x-forge-group-name": ["pets"], "x-forge-method-name": "get", + "x-forge-namespace": ["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": "é 😀", @@ -152,9 +152,9 @@ const FEATURES: &str = r##"{ }, "delete": { "x-forge-aliases": [ - {"x-forge-group-name": ["pets"], "x-forge-method-name": "remove", "x-forge-availability": "deprecated", + {"x-forge-namespace": ["pets"], "x-forge-method-name": "remove", "x-forge-availability": "deprecated", "x-forge-require-confirmation": "This deletes the pet.", "x-alias": [1]}, - {"x-forge-group-name": ["admin", "pets"], "x-forge-method-name": "delete", "x-forge-require-confirmation": ""} + {"x-forge-namespace": ["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-forge-group-name": ["pets", "photos"], "x-forge-method-name": "upload", + "x-forge-namespace": ["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."}}, @@ -351,7 +351,7 @@ fn bench() { for path in std::env::split_paths(&paths).filter(|path| !path.as_os_str().is_empty()) { let json = std::fs::read(&path).unwrap(); let forge = parse(&json); - let groups = forge.groups().count(); + let namespaces = forge.namespaces().count(); let start = Instant::now(); let files = wasm(&forge, &plugin).unwrap(); @@ -365,7 +365,7 @@ fn bench() { ); println!( - "\n{} ({} MB, {groups} groups, {} files, identical output and dump)", + "\n{} ({} MB, {namespaces} namespaces, {} files, identical output and dump)", path.display(), json.len() / 1_000_000, files.len(), @@ -385,7 +385,7 @@ fn bench() { row(&name, native, wasm); } - // `run` calls `generate` alone before the groups, so it bounds + // `run` calls `generate` alone before the namespaces, so it bounds // how much the threads can help. let native = median(|| { let files = bin::ts::Generator.generate(&forge).unwrap(); @@ -394,20 +394,20 @@ fn bench() { let wasm = median(|| plugin.generate(&forge).unwrap().for_each(drop)); row("serial `generate` only", native, wasm); - // A group runs on one thread, so the slowest one bounds it too. + // A namespace runs on one thread, so the slowest one bounds it too. let (native, wasm) = forge - .groups() - .map(|group| { + .namespaces() + .map(|namespace| { let native = median(|| { - let files = bin::ts::Generator.generate_group(group).unwrap(); + let files = bin::ts::Generator.generate_namespace(namespace).unwrap(); files.for_each(|file| drop(file.content().unwrap())); }); - let wasm = median(|| plugin.generate_group(group).unwrap().for_each(drop)); + let wasm = median(|| plugin.generate_namespace(namespace).unwrap().for_each(drop)); (native, wasm) }) .max() .unwrap(); - row("slowest group", native, wasm); + row("slowest namespace", native, wasm); } } diff --git a/tests/wasm/dump.rs b/tests/wasm/dump.rs index 86c788b1d..7a4b16e3e 100644 --- a/tests/wasm/dump.rs +++ b/tests/wasm/dump.rs @@ -5,7 +5,7 @@ use std::fmt::Write; -use forge::{Content, Field, Forge, Group, Method, Model, Name, Object, Type}; +use forge::{Content, Field, Forge, Method, Model, Name, Namespace, Object, Type}; /// Returns everything `forge` has, as text. pub fn dump(forge: &Forge) -> String { @@ -17,8 +17,8 @@ pub fn dump(forge: &Forge) -> String { for model in forge.models() { self::model(o, 0, model); } - for group in forge.groups() { - self::group(o, group); + for namespace in forge.namespaces() { + self::namespace(o, namespace); } out } @@ -31,32 +31,34 @@ fn name(name: &Name) -> String { name.words().join(" ") } -fn group(o: &mut String, group: Group<'_>) { - let path: Vec = group.path().map(name).collect(); - line(o, 0, format_args!("group {path:?} {:?}", group.description())); - let parent = group.parent().map(|parent| name(parent.name())); - let children: Vec = group.children().map(|child| name(child.name())).collect(); - let descendants: Vec = group.descendants().map(|d| name(d.name())).collect(); +fn namespace(o: &mut String, namespace: Namespace<'_>) { + let path: Vec = namespace.path().map(name).collect(); + line(o, 0, format_args!("namespace {path:?} {:?}", namespace.description())); + let parent = namespace.parent().map(|parent| name(parent.name())); + let children: Vec = namespace.children().map(|child| name(child.name())).collect(); + let descendants: Vec = namespace.descendants().map(|d| name(d.name())).collect(); line( o, 1, format_args!("parent {parent:?} children {children:?} descendants {descendants:?}"), ); - for model in group.models() { + for model in namespace.models() { self::model(o, 1, model); } - for method in group.methods() { + for method in namespace.methods() { self::method(o, method); } } fn model(o: &mut String, depth: usize, model: Model<'_>) { - let group = model.group().map(|group| group.path().map(name).collect::>()); + let namespace = model + .namespace() + .map(|namespace| namespace.path().map(name).collect::>()); line( o, depth, format_args!( - "model {:?} name {:?} group {group:?} description {:?} deprecated {}", + "model {:?} name {:?} namespace {namespace:?} description {:?} deprecated {}", model.schema_name(), name(model.name()), model.description(), @@ -75,7 +77,7 @@ fn method(o: &mut String, method: Method<'_>) { name(method.name()), method.http_method().as_str(), method.path(), - name(method.group().name()), + name(method.namespace().name()), ), ); line( diff --git a/tests/wasm/ts/src/lib.rs b/tests/wasm/ts/src/lib.rs index b289aa574..44735b825 100644 --- a/tests/wasm/ts/src/lib.rs +++ b/tests/wasm/ts/src/lib.rs @@ -34,7 +34,7 @@ use wit::forge::plugin::forge as host; pub mod plugin { use std::borrow::Cow; - use crate::{Forge, Group}; + use crate::{Forge, Namespace}; pub mod ts; @@ -49,7 +49,7 @@ pub mod plugin { Ok(std::iter::empty()) } - fn generate_group(&self, _: Group<'_>) -> Result, Err> { + fn generate_namespace(&self, _: Namespace<'_>) -> Result, Err> { Ok(std::iter::empty()) } } @@ -94,11 +94,11 @@ impl wit::Guest for Component { render(bin::ts::Generator.generate(&forge).map_err(|err| err.to_string())?) } - fn generate_group(group: u32) -> Result, String> { + fn generate_namespace(namespace: u32) -> Result, String> { let forge = Forge::fetch(); render( bin::ts::Generator - .generate_group(forge.group(group)) + .generate_namespace(forge.namespace(namespace)) .map_err(|err| err.to_string())?, ) } @@ -128,10 +128,10 @@ pub struct Forge { pub description: String, pub base_url: String, - groups: Vec, + namespaces: Vec, top_models: Vec, - group_data: FrozenMap>, + namespace_data: FrozenMap>, method_data: FrozenMap>, model_data: FrozenMap>, type_data: FrozenMap>, @@ -144,25 +144,25 @@ impl Forge { version: info.version, description: info.description, base_url: info.base_url, - groups: host::groups(), + namespaces: host::namespaces(), top_models: host::models(), - group_data: FrozenMap::new(), + namespace_data: FrozenMap::new(), method_data: FrozenMap::new(), model_data: FrozenMap::new(), type_data: FrozenMap::new(), } } - pub fn groups(&self) -> impl Iterator> { - self.groups.iter().map(|&id| self.group(id)) + pub fn namespaces(&self) -> impl Iterator> { + self.namespaces.iter().map(|&id| self.namespace(id)) } pub fn models(&self) -> impl ExactSizeIterator> { self.top_models.iter().map(|&id| self.model(id)) } - fn group(&self, id: u32) -> Group<'_> { - Group { forge: self, id } + fn namespace(&self, id: u32) -> Namespace<'_> { + Namespace { forge: self, id } } fn method(&self, id: u32) -> Method<'_> { @@ -173,23 +173,23 @@ impl Forge { Model { forge: self, id } } - fn depth_first<'f>(&'f self, ids: &'f [u32]) -> impl Iterator> { + fn depth_first<'f>(&'f self, ids: &'f [u32]) -> impl Iterator> { let mut stack: Vec = ids.iter().rev().copied().collect(); iter::from_fn(move || { - let group = self.group(stack.pop()?); - stack.extend(group.data().children.iter().rev()); - Some(group) + let namespace = self.namespace(stack.pop()?); + stack.extend(namespace.data().children.iter().rev()); + Some(namespace) }) } - fn group_data(&self, id: u32) -> &GroupData { - if let Some(data) = self.group_data.get(&id) { + fn namespace_data(&self, id: u32) -> &NamespaceData { + if let Some(data) = self.namespace_data.get(&id) { return data; } - let g = host::get_group(id); - self.group_data.insert( + let g = host::get_namespace(id); + self.namespace_data.insert( id, - Box::new(GroupData { + Box::new(NamespaceData { name: Name::from_words(g.name), description: g.description, path: g.path, @@ -209,7 +209,7 @@ impl Forge { id, Box::new(MethodData { name: Name::from_words(m.name), - group: m.group, + namespace: m.namespace, http_method: match m.http_method { host::HttpMethod::Get => HttpMethod::Get, host::HttpMethod::Put => HttpMethod::Put, @@ -265,7 +265,7 @@ impl Forge { Box::new(ModelData { schema_name: m.schema_name, name: Name::from_words(m.name), - group: m.group, + namespace: m.namespace, description: m.description, deprecated: m.deprecated, ty: m.ty, @@ -372,7 +372,7 @@ fn response(r: host::ResponseInfo) -> ResponseData { } } -struct GroupData { +struct NamespaceData { name: Name, description: String, path: Vec, @@ -383,7 +383,7 @@ struct GroupData { struct MethodData { name: Name, - group: u32, + namespace: u32, http_method: HttpMethod, path: String, summary: String, @@ -423,7 +423,7 @@ struct ContentData { struct ModelData { schema_name: String, name: Name, - group: Option, + namespace: Option, description: String, deprecated: bool, ty: u32, @@ -485,12 +485,12 @@ struct DiscriminatorData { // The public API, as src/lib.rs, src/types.rs and src/method.rs have it. #[derive(Clone, Copy)] -pub struct Group<'f> { +pub struct Namespace<'f> { forge: &'f Forge, id: u32, } -impl<'f> Group<'f> { +impl<'f> Namespace<'f> { pub fn name(self) -> &'f Name { &self.data().name } @@ -501,22 +501,22 @@ impl<'f> Group<'f> { pub fn path(self) -> impl ExactSizeIterator + DoubleEndedIterator { let forge = self.forge; - self.data().path.iter().map(move |&id| forge.group(id).name()) + self.data().path.iter().map(move |&id| forge.namespace(id).name()) } - pub fn parent(self) -> Option> { + pub fn parent(self) -> Option> { let [.., parent, _] = self.data().path[..] else { return None; }; - Some(self.forge.group(parent)) + Some(self.forge.namespace(parent)) } - pub fn children(self) -> impl ExactSizeIterator> { + pub fn children(self) -> impl ExactSizeIterator> { let forge = self.forge; - self.data().children.iter().map(move |&id| forge.group(id)) + self.data().children.iter().map(move |&id| forge.namespace(id)) } - pub fn descendants(self) -> impl Iterator> { + pub fn descendants(self) -> impl Iterator> { self.forge.depth_first(&self.data().children) } @@ -530,29 +530,29 @@ impl<'f> Group<'f> { self.data().models.iter().map(move |&id| forge.model(id)) } - fn data(self) -> &'f GroupData { - self.forge.group_data(self.id) + fn data(self) -> &'f NamespaceData { + self.forge.namespace_data(self.id) } } -impl PartialEq for Group<'_> { +impl PartialEq for Namespace<'_> { fn eq(&self, other: &Self) -> bool { std::ptr::eq(self.forge, other.forge) && self.id == other.id } } -impl Eq for Group<'_> {} +impl Eq for Namespace<'_> {} -impl std::hash::Hash for Group<'_> { +impl std::hash::Hash for Namespace<'_> { fn hash(&self, state: &mut H) { std::ptr::hash(self.forge, state); self.id.hash(state); } } -impl fmt::Debug for Group<'_> { +impl fmt::Debug for Namespace<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Group") + f.debug_struct("Namespace") .field("path", &self.path().collect::>()) .finish() } @@ -569,8 +569,8 @@ impl<'f> Method<'f> { &self.data().name } - pub fn group(self) -> Group<'f> { - self.forge.group(self.data().group) + pub fn namespace(self) -> Namespace<'f> { + self.forge.namespace(self.data().namespace) } pub fn http_method(self) -> &'f HttpMethod { @@ -679,7 +679,7 @@ impl std::hash::Hash for Method<'_> { impl fmt::Debug for Method<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Method") - .field("group", &self.group().path().collect::>()) + .field("namespace", &self.namespace().path().collect::>()) .field("name", self.name()) .field("http_method", self.http_method()) .field("path", &self.path()) @@ -756,8 +756,8 @@ impl<'f> Model<'f> { &self.data().name } - pub fn group(self) -> Option> { - Some(self.forge.group(self.data().group?)) + pub fn namespace(self) -> Option> { + Some(self.forge.namespace(self.data().namespace?)) } pub fn description(self) -> &'f str { @@ -801,7 +801,10 @@ impl fmt::Display for Model<'_> { impl fmt::Debug for Model<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Model") - .field("group", &self.group().map(|group| group.path().collect::>())) + .field( + "namespace", + &self.namespace().map(|namespace| namespace.path().collect::>()), + ) .field("schema_name", &self.schema_name()) .finish() } diff --git a/tests/wasm/ts/wit/forge.wit b/tests/wasm/ts/wit/forge.wit index 7a489f74e..f15557753 100644 --- a/tests/wasm/ts/wit/forge.wit +++ b/tests/wasm/ts/wit/forge.wit @@ -1,5 +1,5 @@ -package forge:plugin@0.3.0; +package forge:plugin@0.4.0; /// Read-only access to the forge a plugin runs on, which the host keeps. /// Each id is an index the host gave out. An id the host never gave out @@ -8,7 +8,7 @@ package forge:plugin@0.3.0; /// Everything a part has is returned in one call, and orders and meanings /// are as the Rust API documents them. interface forge { - type group-id = u32; + type namespace-id = u32; type method-id = u32; type model-id = u32; type type-id = u32; @@ -30,19 +30,19 @@ interface forge { /// How many of each part the forge has. Ids run from 0 to the count. record forge-counts { - groups: u32, + namespaces: u32, methods: u32, models: u32, types: u32, } - record group-info { + record namespace-info { name: name, description: string, - /// The groups from the top level down to this one, ending with it. - path: list, - /// The groups directly inside, sorted by name. - children: list, + /// The namespaces from the top level down to this one, ending with it. + path: list, + /// The namespaces directly inside, sorted by name. + children: list, /// The methods directly inside, sorted by name. methods: list, models: list, @@ -89,7 +89,7 @@ interface forge { record method-info { name: name, - group: group-id, + namespace: namespace-id, http-method: http-method, path: string, summary: string, @@ -117,7 +117,7 @@ interface forge { record model-info { schema-name: string, name: name, - group: option, + namespace: option, description: string, deprecated: bool, ty: type-id, @@ -191,11 +191,11 @@ interface forge { info: func() -> forge-info; counts: func() -> forge-counts; - /// Every group, depth first. - groups: func() -> list; - /// The models at the top level, in no group. + /// Every namespace, depth first. + namespaces: func() -> list; + /// The models at the top level, in no namespace. models: func() -> list; - get-group: func(id: group-id) -> group-info; + get-namespace: func(id: namespace-id) -> namespace-info; get-method: func(id: method-id) -> method-info; get-model: func(id: model-id) -> model-info; get-type: func(id: type-id) -> type-info; @@ -206,7 +206,7 @@ interface forge { world plugin { import forge; - use forge.{group-id}; + use forge.{namespace-id}; /// A generated file: its path relative to the output directory, using /// `/` as a separator, and its content. A file with no content is not @@ -218,7 +218,7 @@ world plugin { /// Generates files for the forge. Called exactly once. export generate: func() -> result, string>; - /// Generates files for a group. Called exactly once for each group, in - /// parallel with other groups, each in an instance of its own. - export generate-group: func(group: group-id) -> result, string>; + /// Generates files for a namespace. Called exactly once for each namespace, in + /// parallel with other namespaces, each in an instance of its own. + export generate-namespace: func(namespace: namespace-id) -> result, string>; }