From 010ab7ecffef7178aeb9a268e7358fd49471de0e Mon Sep 17 00:00:00 2001 From: Niels Swimberghe Date: Mon, 2 Nov 2026 18:23:05 -0500 Subject: [PATCH] test: verify normalized metadata parser handoff --- packages/forge/README.md | 4 +- tests/normalize/normalize_cli.rs | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/forge/README.md b/packages/forge/README.md index 75d6b62a9..19ab87450 100644 --- a/packages/forge/README.md +++ b/packages/forge/README.md @@ -19,8 +19,8 @@ Pass `-` instead of a path to read the document from standard input. Run ## Normalizing a document `forge normalize` rewrites metadata extensions from other tools into Forge's -`x-forge-*` vocabulary. This command does not change which extensions Forge -reads; adopting the native vocabulary in the parser is a separate change. +`x-forge-*` vocabulary. Forge reads that native vocabulary directly: vendor +extensions have no parser effect until they are normalized. ```sh # Print the document with its keys rewritten. Nothing is changed. diff --git a/tests/normalize/normalize_cli.rs b/tests/normalize/normalize_cli.rs index edc6de409..7186f83a3 100644 --- a/tests/normalize/normalize_cli.rs +++ b/tests/normalize/normalize_cli.rs @@ -5,6 +5,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; +use forge::{Availability, Forge, Name, Type}; use serde_json::Value; /// A document with one operation, written on one line, with no final newline. @@ -16,6 +17,36 @@ const NATIVE: &str = r#"{"openapi":"3.0.3","info":{"title":"t","version":"1"},"p /// 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}}}}}}"##; +/// A document that uses every compatibility key Forge knows how to normalize. +const COMPATIBILITY: &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", + "x-fern-ignore": false, + "x-fern-availability": "beta", + "parameters": [{ + "name": "page", + "in": "query", + "x-fern-parameter-name": "page_number", + "schema": {"type": "integer"} + }], + "responses": {"200": { + "description": "ok", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/S"}}} + }} + }}}, + "components": {"schemas": {"S": { + "type": "object", + "x-fern-type-name": "Secret", + "properties": { + "value": {"type": "string", "x-fern-property-name": "token", "x-sensitive": true}, + "hidden": {"type": "string", "x-stainless-skip": true} + } + }}} +}"##; + /// A folder of its own to write files in, which is empty. fn dir(name: &str) -> PathBuf { let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("normalize").join(name); @@ -134,6 +165,40 @@ fn standard_input_is_written_to_standard_output() { assert_eq!(text(&out.stdout), NATIVE); } +#[test] +fn normalized_compatibility_keys_are_read_by_forge() { + let out = normalize(&["-"], COMPATIBILITY); + + succeeded(&out); + let forge: Forge = serde_json::from_slice(&out.stdout).unwrap(); + let group = forge.groups().find(|group| *group.name() == Name::new("a")).unwrap(); + let method = group + .methods() + .find(|method| *method.name() == Name::new("get")) + .unwrap(); + assert_eq!(method.availability(), Some(Availability::Beta)); + + let Type::Object(request) = method.request().unwrap() else { + panic!("the normalized parameter is not an object"); + }; + let parameter = request.fields().next().unwrap(); + assert_eq!(parameter.wire_name(), "page"); + assert_eq!(parameter.name(), &Name::new("page_number")); + + let Type::Model(model) = method.responses().next().unwrap().json().unwrap().ty() else { + panic!("the normalized response is not a model"); + }; + assert_eq!(model.name(), &Name::new("Secret")); + let Type::Object(object) = model.ty() else { + panic!("the normalized model is not an object"); + }; + let fields: Vec<_> = object.fields().collect(); + assert_eq!(fields.len(), 1, "x-stainless-skip removes the other field"); + assert_eq!(fields[0].wire_name(), "value"); + assert_eq!(fields[0].name(), &Name::new("token")); + assert!(fields[0].is_sensitive()); +} + #[test] fn from_rewrites_only_the_selected_compatibility_sources_and_may_be_repeated() { let out = normalize(&["--from", "fern", "--from", "cloudflare", "-"], SOURCES);