From 54167a0071a367ab8271da79d3f6d9bc8c168840 Mon Sep 17 00:00:00 2001 From: Sri Krishna <7254698+srikrsna@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:58:19 +0530 Subject: [PATCH 1/2] Simplify source and normalize trait --- src/bin/forge/normalize.rs | 34 ++-- src/normalize/mod.rs | 109 ++----------- src/normalize/vendor/cloudflare.rs | 18 ++- src/normalize/vendor/fern.rs | 53 +++---- src/normalize/vendor/mod.rs | 242 ++++++++++++++++------------- src/normalize/vendor/stainless.rs | 18 ++- src/normalize/vendor/tests.rs | 101 ++++++------ src/normalize/walk.rs | 50 ++++-- 8 files changed, 299 insertions(+), 326 deletions(-) diff --git a/src/bin/forge/normalize.rs b/src/bin/forge/normalize.rs index 04b803e23..0f030c366 100644 --- a/src/bin/forge/normalize.rs +++ b/src/bin/forge/normalize.rs @@ -7,12 +7,7 @@ use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use forge::Err; -use forge::normalize::{Normalized, Options, Report, Sources, normalize}; - -fn source(name: &str) -> Result { - Sources::named(name) - .ok_or_else(|| format!("unknown compatibility source {name:?}; expected cloudflare, fern, or stainless")) -} +use forge::normalize::{Normalized, Options, Report, Source, normalize, sources}; /// What `forge normalize` is given, which says what it does in `main.rs`. #[derive(clap::Args)] @@ -34,7 +29,7 @@ pub struct Args { /// Rewrite only keys from cloudflare, fern, or stainless. May be repeated. /// Without it, keys from every source are rewritten. #[arg(long = "from", value_parser = source, value_name = "SOURCE")] - from: Vec, + from: Vec<&'static dyn Source>, /// Say what would be rewritten, and write nothing. It fails if there is /// anything to rewrite, to check that documents are normalized. #[arg(long)] @@ -64,17 +59,19 @@ struct Say { } pub fn run(args: Args) -> Result<(), Err> { - let sources = if args.from.is_empty() { - Sources::all() - } else { - args.from - .into_iter() - .reduce(Sources::union) - .expect("at least one source was given") - }; + let mut selected: Vec<&dyn Source> = Vec::new(); + for source in args.from { + if !selected.iter().any(|chosen| chosen.name() == source.name()) { + selected.push(source); + } + } let options = Options { leave_old_extensions: args.leave_old_extensions, - sources, + source: if selected.is_empty() { + &sources() + } else { + &selected.as_slice() + }, }; let say = Say { check: args.check, @@ -145,6 +142,11 @@ pub fn run(args: Args) -> Result<(), Err> { Ok(()) } +fn source(name: &str) -> Result<&'static dyn Source, String> { + forge::normalize::source(name) + .ok_or_else(|| format!("unknown compatibility source {name:?}; expected cloudflare, fern, or stainless")) +} + /// Returns `one` if `n` is 1, and `many` if it is not. fn plural<'a>(n: usize, one: &'a str, many: &'a str) -> &'a str { if n == 1 { one } else { many } diff --git a/src/normalize/mod.rs b/src/normalize/mod.rs index c16791c41..036e6d626 100644 --- a/src/normalize/mod.rs +++ b/src/normalize/mod.rs @@ -1,7 +1,7 @@ //! Rewrites the extensions other tools' metadata is written with into the //! native `x-forge-*` ones, in a JSON document. //! -//! The key a tool writes is found with the tables of [`vendor`], at the kinds +//! The key a tool writes is found with the sources of [`vendor`], at the kinds //! of OpenAPI object where its native replacement belongs. A key elsewhere is //! left alone, since rewriting it would give it a meaning it did not have. //! @@ -23,115 +23,38 @@ use serde::Serialize; use serde_json::Value; use serde_json::ser::PrettyFormatter; -/// The sources of compatibility keys that [`normalize`] rewrites. -#[derive(Clone)] -pub struct Sources(Vec<&'static dyn vendor::Source>); +use vendor::Detail; +pub use vendor::{Change, Native, Normalize, Source}; -impl Sources { - /// Every compatibility source. - pub fn all() -> Sources { - Sources(vendor::ALL.to_vec()) - } - - /// Returns the source called `name`, if it exists. - pub fn named(name: &str) -> Option { - vendor::ALL - .iter() - .find(|source| source.name() == name) - .map(|source| Sources(vec![*source])) - } - - /// Returns these sources and `other` together. - pub fn union(mut self, other: Sources) -> Sources { - for source in other.0 { - if !self.0.iter().any(|selected| selected.name() == source.name()) { - self.0.push(source); - } - } - self - } - - fn find( - &self, - key: &str, - rules: impl Fn(&dyn vendor::Source) -> &'static [vendor::Rule], - ) -> Option<&'static dyn vendor::Normalize> { - self.0 - .iter() - .flat_map(|source| rules(*source)) - .find(|(original, _)| *original == key) - .map(|(_, normalize)| *normalize) - } - - fn definition(&self, key: &str) -> Option<&'static dyn vendor::Normalize> { - self.find(key, |source| source.definition()) - } - - fn parameter(&self, key: &str) -> Option<&'static dyn vendor::Normalize> { - self.find(key, |source| source.parameter()) - } - - fn schema(&self, key: &str) -> Option<&'static dyn vendor::Normalize> { - self.find(key, |source| source.schema()) - } +/// Every compatibility source. As a slice, they are a [`Source`] together. +pub fn sources() -> &'static [&'static dyn Source] { + vendor::ALL } -impl fmt::Debug for Sources { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list() - .entries(self.0.iter().map(|source| source.name())) - .finish() - } +/// Returns the compatibility source called `name`, if there is one. +pub fn source(name: &str) -> Option<&'static dyn Source> { + vendor::ALL.iter().find(|source| source.name() == name).copied() } /// What [`normalize`] does. -#[derive(Clone, Debug)] -pub struct Options { +#[derive(Clone, Copy, Debug)] +pub struct Options<'a> { /// Keeps each key that is rewritten, beside the native key that replaces /// it, for what still reads the old one. pub leave_old_extensions: bool, - /// The compatibility sources to rewrite. - pub sources: Sources, + /// The compatibility source to rewrite, which may be several combined. + pub source: &'a dyn Source, } -impl Default for Options { - fn default() -> Options { +impl Default for Options<'_> { + fn default() -> Self { Options { leave_old_extensions: false, - sources: Sources::all(), + source: &vendor::ALL, } } } -/// An optional explanation of a normalization change. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -struct Detail { - one: &'static str, - many: &'static str, -} - -impl Detail { - const fn new(one: &'static str, many: &'static str) -> Detail { - Detail { one, many } - } -} - -/// A change made by one normalizer. -#[derive(Clone, Copy, Debug)] -struct Change { - detail: Option, -} - -impl Change { - const fn plain() -> Change { - Change { detail: None } - } - - const fn described(detail: Detail) -> Change { - Change { detail: Some(detail) } - } -} - /// What [`normalize`] did. #[derive(Debug, Default)] pub struct Report { diff --git a/src/normalize/vendor/cloudflare.rs b/src/normalize/vendor/cloudflare.rs index 0f50ef4aa..d6876d404 100644 --- a/src/normalize/vendor/cloudflare.rs +++ b/src/normalize/vendor/cloudflare.rs @@ -1,18 +1,20 @@ //! The adapter for Cloudflare's compatibility extensions. -use super::{Rename, Rule, Source}; +use std::borrow::Cow; -pub(super) struct Cloudflare; +use super::{Normalize, Source}; -static SENSITIVE: Rename = Rename::same("x-forge-sensitive"); -static SCHEMA: [Rule; 1] = [("x-sensitive", &SENSITIVE)]; +pub(super) struct Cloudflare; impl Source for Cloudflare { - fn name(&self) -> &'static str { - "cloudflare" + fn name(&self) -> Cow<'static, str> { + Cow::Borrowed("cloudflare") } - fn schema(&self) -> &'static [Rule] { - &SCHEMA + fn schema(&self, key: &str) -> Option<&dyn Normalize> { + match key { + "x-sensitive" => Some(&"x-forge-sensitive"), + _ => None, + } } } diff --git a/src/normalize/vendor/fern.rs b/src/normalize/vendor/fern.rs index 0176b6d18..6b3810b60 100644 --- a/src/normalize/vendor/fern.rs +++ b/src/normalize/vendor/fern.rs @@ -1,43 +1,38 @@ //! The adapter for Fern's `x-fern-*` extensions. -use super::{Rename, Rule, Source}; +use std::borrow::Cow; -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 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"); -static MODEL_NAME: Rename = Rename::same("x-forge-model-name"); -static PROPERTY_NAME: Rename = Rename::same("x-forge-property-name"); +use super::{Group, Normalize, Source}; -static DEFINITION: [Rule; 4] = [ - ("x-fern-sdk-method-name", &METHOD_NAME), - ("x-fern-sdk-group-name", &GROUP_NAME), - ("x-fern-ignore", &SKIP), - ("x-fern-availability", &AVAILABILITY), -]; -static PARAMETER: [Rule; 1] = [("x-fern-parameter-name", &PARAMETER_NAME)]; -static SCHEMA: [Rule; 2] = [ - ("x-fern-type-name", &MODEL_NAME), - ("x-fern-property-name", &PROPERTY_NAME), -]; +pub(super) struct Fern; impl Source for Fern { - fn name(&self) -> &'static str { - "fern" + fn name(&self) -> Cow<'static, str> { + Cow::Borrowed("fern") } - fn definition(&self) -> &'static [Rule] { - &DEFINITION + fn definition(&self, key: &str) -> Option<&dyn Normalize> { + match key { + "x-fern-sdk-method-name" => Some(&"x-forge-method-name"), + "x-fern-sdk-group-name" => Some(&Group("x-forge-group-name")), + "x-fern-ignore" => Some(&"x-forge-skip"), + "x-fern-availability" => Some(&"x-forge-availability"), + _ => None, + } } - fn parameter(&self) -> &'static [Rule] { - &PARAMETER + fn parameter(&self, key: &str) -> Option<&dyn Normalize> { + match key { + "x-fern-parameter-name" => Some(&"x-forge-parameter-name"), + _ => None, + } } - fn schema(&self) -> &'static [Rule] { - &SCHEMA + fn schema(&self, key: &str) -> Option<&dyn Normalize> { + match key { + "x-fern-type-name" => Some(&"x-forge-model-name"), + "x-fern-property-name" => Some(&"x-forge-property-name"), + _ => None, + } } } diff --git a/src/normalize/vendor/mod.rs b/src/normalize/vendor/mod.rs index 34000422e..58db3fe70 100644 --- a/src/normalize/vendor/mod.rs +++ b/src/normalize/vendor/mod.rs @@ -2,14 +2,14 @@ //! //! A [`Source`] names the normalizers that apply to operations, parameters, //! and schemas. A key is listed only at the OpenAPI object kind where Forge -//! reads its native replacement; elsewhere it is left alone. [`Normalize`] -//! owns the complete mutation for one compatibility key, so a future -//! normalizer is not required to be a one-to-one rename. +//! reads its native replacement; elsewhere it is left alone. A [`Normalize`] +//! gives the [`Native`] form of an extension: the key that replaces it and +//! its value; the walk renames the key. //! //! To add a compatibility source, implement [`Source`] in a module here and -//! add its value to [`ALL`]. To add a key to an existing source, add its -//! [`Rule`] to the applicable table. Normalization introduces no new Forge -//! semantics: the corresponding native key must already exist. +//! add its value to [`ALL`]. To add a key to an existing source, return its +//! normalizer from the applicable method. Normalization introduces no new +//! Forge semantics: the corresponding native key must already exist. mod cloudflare; mod fern; @@ -17,145 +17,127 @@ mod stainless; #[cfg(test)] mod tests; -use serde_json::{Map, Value}; +use std::borrow::Cow; +use std::fmt; -use super::{Change, Detail, Options}; +use serde_json::Value; -/// One compatibility key and the normalizer that owns it. -pub(super) type Rule = (&'static str, &'static dyn Normalize); +/// Every available compatibility source. +pub(super) static ALL: &[&dyn Source] = &[&cloudflare::Cloudflare, &fern::Fern, &stainless::Stainless]; /// A source of compatibility extensions. -pub(super) trait Source: Sync { +pub trait Source: Sync { /// The name accepted by `--from`. - fn name(&self) -> &'static str; + fn name(&self) -> Cow<'static, str>; - /// Normalizers for operations and aliases. - fn definition(&self) -> &'static [Rule] { - &[] + /// The normalizer for `key` on operations and aliases, if there is one. + fn definition(&self, _key: &str) -> Option<&dyn Normalize> { + None } - /// Normalizers for Parameter Objects. - fn parameter(&self) -> &'static [Rule] { - &[] + /// The normalizer for `key` on Parameter Objects, if there is one. + fn parameter(&self, _key: &str) -> Option<&dyn Normalize> { + None } - /// Normalizers for Schema Objects. - fn schema(&self) -> &'static [Rule] { - &[] + /// The normalizer for `key` on Schema Objects, if there is one. + fn schema(&self, _key: &str) -> Option<&dyn Normalize> { + None } } -/// Normalizes one compatibility extension in its containing object. -pub(super) trait Normalize: Sync { - /// Returns the change that was made, or none when this value has no native - /// representation and the object is left alone. - fn normalize(&self, container: &mut Map, original: &str, options: &Options) -> Option; +/// Normalizes one compatibility extension into its [`Native`] form. +pub trait Normalize: Sync { + /// The native form of an extension whose value is `value`, or none when + /// it has no native form and the extension is left alone. + fn normalize(&self, value: &Value) -> Option; +} + +/// An extension in its native form. +pub struct Native { + /// The native key that replaces the compatibility key. + pub(super) key: &'static str, + /// The value in its native form, or none when it is kept as it is. + pub(super) value: Option, + pub(super) change: Change, } -/// Renames one extension to its native equivalent. -/// -/// Every current compatibility rule is a rename, with one of the value forms -/// below. A future rule that restructures an object can implement [`Normalize`] -/// directly instead. -pub(super) struct Rename { - native: &'static str, - form: Form, +impl Native { + /// Only the key renamed to `key`, with the value kept as it is. + const fn renamed(key: &'static str) -> Native { + Native { + key, + value: None, + change: Change::plain(), + } + } } -#[derive(Clone, Copy, PartialEq, Eq)] -enum Form { - Same, - Group, - BoolOrTargets, +/// A change made by one normalizer. +#[derive(Clone, Copy, Debug)] +pub struct Change { + pub(super) detail: Option, } -impl Rename { - pub(super) const fn same(native: &'static str) -> Rename { - Rename { - native, - form: Form::Same, - } +impl Change { + pub(super) const fn plain() -> Change { + Change { detail: None } } - pub(super) const fn group(native: &'static str) -> Rename { - Rename { - native, - form: Form::Group, - } + pub(super) const fn described(detail: Detail) -> Change { + Change { detail: Some(detail) } } +} - pub(super) const fn bool_or_targets(native: &'static str) -> Rename { - Rename { - native, - form: Form::BoolOrTargets, - } +/// An optional explanation of a normalization change. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(super) struct Detail { + pub(super) one: &'static str, + pub(super) many: &'static str, +} + +impl Detail { + const fn new(one: &'static str, many: &'static str) -> Detail { + Detail { one, many } } +} - fn supports(self: &Rename, value: &Value) -> bool { - self.form != Form::BoolOrTargets || !value.is_array() +/// A plain rename: the native key, with the value kept as it is. +impl Normalize for &'static str { + fn normalize(&self, _: &Value) -> Option { + Some(Native::renamed(self)) } +} - fn value(self: &Rename, value: Value) -> (Value, Option) { - let Form::Group = self.form else { - return (value, None); - }; - let Value::Array(names) = &value else { - return (value, None); - }; - let Some(path) = dotted(names) else { - return (value, None); +/// A group path, with an array of names written as a dotted string when each +/// name reads back as it is. +pub(super) struct Group(pub(super) &'static str); + +impl Normalize for Group { + fn normalize(&self, value: &Value) -> Option { + let Some(path) = value.as_array().and_then(|names| dotted(names)) else { + return Some(Native::renamed(self.0)); }; - ( - Value::String(path), - Some(Detail::new( + Some(Native { + key: self.0, + value: Some(Value::String(path)), + change: Change::described(Detail::new( "group path written as a dotted string", "group paths written as dotted strings", )), - ) + }) } } -impl Normalize for Rename { - fn normalize(&self, container: &mut Map, original: &str, options: &Options) -> Option { - let value = container.get(original)?; - if !self.supports(value) { - return None; - } +/// A skip that is a boolean, or a list of targets, which has no native form. +pub(super) struct BoolOrTargets(pub(super) &'static str); - // The native value wins when both are present. Keeping compatibility - // keys makes this object already normalized; otherwise the old key is - // removed without touching the native one. - if container.contains_key(self.native) { - if options.leave_old_extensions { - return None; - } - container.shift_remove(original); - return Some(Change::plain()); - } - - let at = container - .keys() - .position(|key| key == original) - .expect("the compatibility key was found in its container"); - let original_value = if options.leave_old_extensions { - value.clone() - } else { - container - .shift_remove(original) - .expect("the compatibility key was found in its container") - }; - let (native_value, detail) = self.value(original_value); - container.shift_insert(at, self.native.to_owned(), native_value); - Some(match detail { - Some(detail) => Change::described(detail), - None => Change::plain(), - }) +impl Normalize for BoolOrTargets { + fn normalize(&self, value: &Value) -> Option { + (!value.is_array()).then(|| Native::renamed(self.0)) } } -/// Every available compatibility source. -pub(super) static ALL: [&dyn Source; 3] = [&cloudflare::Cloudflare, &fern::Fern, &stainless::Stainless]; - /// Returns the names of an array joined by dots, if each is a name that a /// dotted string reads back as it is. fn dotted(names: &[Value]) -> Option { @@ -169,3 +151,49 @@ fn dotted(names: &[Value]) -> Option { } Some(path) } + +/// Many sources combined into one: the first that has a normalizer for a key +/// wins. +impl Source for [S] { + /// The names of the sources, separated by commas. + fn name(&self) -> Cow<'static, str> { + Cow::Owned(self.iter().map(Source::name).collect::>().join(", ")) + } + + fn definition(&self, key: &str) -> Option<&dyn Normalize> { + self.iter().find_map(|source| source.definition(key)) + } + + fn parameter(&self, key: &str) -> Option<&dyn Normalize> { + self.iter().find_map(|source| source.parameter(key)) + } + + fn schema(&self, key: &str) -> Option<&dyn Normalize> { + self.iter().find_map(|source| source.schema(key)) + } +} + +/// So that `[&dyn Source]`, a slice of different sources, is a `Source` too. +impl Source for &S { + fn name(&self) -> Cow<'static, str> { + (**self).name() + } + + fn definition(&self, key: &str) -> Option<&dyn Normalize> { + (**self).definition(key) + } + + fn parameter(&self, key: &str) -> Option<&dyn Normalize> { + (**self).parameter(key) + } + + fn schema(&self, key: &str) -> Option<&dyn Normalize> { + (**self).schema(key) + } +} + +impl fmt::Debug for dyn Source + '_ { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.name()) + } +} diff --git a/src/normalize/vendor/stainless.rs b/src/normalize/vendor/stainless.rs index 5b0f77fcb..354c62de6 100644 --- a/src/normalize/vendor/stainless.rs +++ b/src/normalize/vendor/stainless.rs @@ -1,18 +1,20 @@ //! The adapter for Stainless's `x-stainless-*` extensions. -use super::{Rename, Rule, Source}; +use std::borrow::Cow; -pub(super) struct Stainless; +use super::{BoolOrTargets, Normalize, Source}; -static SKIP: Rename = Rename::bool_or_targets("x-forge-skip"); -static SCHEMA: [Rule; 1] = [("x-stainless-skip", &SKIP)]; +pub(super) struct Stainless; impl Source for Stainless { - fn name(&self) -> &'static str { - "stainless" + fn name(&self) -> Cow<'static, str> { + Cow::Borrowed("stainless") } - fn schema(&self) -> &'static [Rule] { - &SCHEMA + fn schema(&self, key: &str) -> Option<&dyn Normalize> { + match key { + "x-stainless-skip" => Some(&BoolOrTargets("x-forge-skip")), + _ => None, + } } } diff --git a/src/normalize/vendor/tests.rs b/src/normalize/vendor/tests.rs index 0d10b60bc..829da9e96 100644 --- a/src/normalize/vendor/tests.rs +++ b/src/normalize/vendor/tests.rs @@ -1,7 +1,6 @@ //! Tests for the private adapter registry. use super::*; -use crate::normalize::{Options, Sources}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Level { @@ -10,42 +9,40 @@ enum Level { Schema, } -fn tables() -> Vec<(&'static dyn Source, Level, &'static str, &'static dyn Normalize)> { - let table = |source: &'static dyn Source, level: Level, table: &'static [Rule]| -> Vec<_> { - table - .iter() - .map(|&(name, normalize)| (source, level, name, normalize)) - .collect() - }; - ALL.iter() - .flat_map(|source| { - [ - table(*source, Level::Definition, source.definition()), - table(*source, Level::Parameter, source.parameter()), - table(*source, Level::Schema, source.schema()), - ] - .concat() - }) - .collect() +/// Every compatibility key, with the source it is from and the level it is +/// read at. +const KEYS: [(&str, Level, &str); 9] = [ + ("cloudflare", Level::Schema, "x-sensitive"), + ("fern", Level::Definition, "x-fern-sdk-method-name"), + ("fern", Level::Definition, "x-fern-sdk-group-name"), + ("fern", Level::Definition, "x-fern-ignore"), + ("fern", Level::Definition, "x-fern-availability"), + ("fern", Level::Parameter, "x-fern-parameter-name"), + ("fern", Level::Schema, "x-fern-type-name"), + ("fern", Level::Schema, "x-fern-property-name"), + ("stainless", Level::Schema, "x-stainless-skip"), +]; + +fn named(name: &str) -> &'static dyn Source { + crate::normalize::source(name).unwrap() } -fn read(sources: &Sources, level: Level, key: &str) -> Option<&'static dyn Normalize> { +fn read<'a>(source: &'a dyn Source, level: Level, key: &str) -> Option<&'a dyn Normalize> { match level { - Level::Definition => sources.definition(key), - Level::Parameter => sources.parameter(key), - Level::Schema => sources.schema(key), + Level::Definition => source.definition(key), + Level::Parameter => source.parameter(key), + Level::Schema => source.schema(key), } } #[test] fn a_key_is_read_only_at_its_level() { - for (source, level, name, _) in tables() { - let sources = Sources::named(source.name()).unwrap(); + for (name, level, key) in KEYS { for other in [Level::Definition, Level::Parameter, Level::Schema] { assert_eq!( - read(&sources, other, name).is_some(), + read(named(name), other, key).is_some(), other == level, - "{name} at {other:?}" + "{key} at {other:?}" ); } } @@ -53,14 +50,12 @@ fn a_key_is_read_only_at_its_level() { #[test] fn a_key_is_read_only_when_its_source_is_selected() { - for (source, level, name, _) in tables() { + for (name, level, key) in KEYS { for selected in ALL { - let sources = Sources::named(selected.name()).unwrap(); assert_eq!( - read(&sources, level, name).is_some(), - selected.name() == source.name(), - "{name} from {}, with {} selected", - source.name(), + read(*selected, level, key).is_some(), + selected.name() == name, + "{key} from {name}, with {} selected", selected.name() ); } @@ -69,39 +64,31 @@ fn a_key_is_read_only_when_its_source_is_selected() { #[test] fn every_adapter_is_in_all_sources() { - let sources = Sources::all(); - for (source, level, name, _) in tables() { - assert!(read(&sources, level, name).is_some(), "{}: {name}", source.name()); + for (name, level, key) in KEYS { + assert!(read(&ALL, level, key).is_some(), "{name}: {key}"); } } #[test] -fn every_rule_writes_a_native_key() { - for (_, _, original, normalize) in tables() { - let mut container = serde_json::Map::from_iter([(original.to_owned(), Value::Bool(true))]); +fn every_normalizer_writes_a_native_key() { + for (_, level, original) in KEYS { + let normalizer = read(&ALL, level, original).unwrap(); + let native = normalizer.normalize(&Value::Bool(true)); assert!( - normalize - .normalize(&mut container, original, &Options::default()) - .is_some() - ); - assert!(!container.contains_key(original), "{original}"); - assert!( - container.keys().all(|key| key.starts_with("x-forge-")), - "{original}: {container:?}" + native.is_some_and(|native| native.key.starts_with("x-forge-")), + "{original}" ); } } #[test] fn only_the_stainless_skip_may_have_no_native_value() { - for (_, _, original, normalize) in tables() { - let mut container = serde_json::Map::from_iter([(original.to_owned(), serde_json::json!(["terraform"]))]); + for (_, level, original) in KEYS { + let normalizer = read(&ALL, level, original).unwrap(); assert_eq!( - normalize - .normalize(&mut container, original, &Options::default()) - .is_none(), + normalizer.normalize(&serde_json::json!(["terraform"])).is_none(), original == "x-stainless-skip", "{original}" ); @@ -110,7 +97,6 @@ fn only_the_stainless_skip_may_have_no_native_value() { #[test] fn the_other_extensions_of_these_tools_are_not_read() { - let sources = Sources::all(); for key in [ "x-fern-enum", "x-fern-audiences", @@ -121,8 +107,13 @@ fn the_other_extensions_of_these_tools_are_not_read() { "x-forge-skip", "x-forge-method-name", ] { - assert!(sources.definition(key).is_none(), "{key}"); - assert!(sources.parameter(key).is_none(), "{key}"); - assert!(sources.schema(key).is_none(), "{key}"); + assert!(ALL.definition(key).is_none(), "{key}"); + assert!(ALL.parameter(key).is_none(), "{key}"); + assert!(ALL.schema(key).is_none(), "{key}"); } } + +#[test] +fn sources_together_are_named_by_their_names_separated_by_commas() { + assert_eq!(ALL.name(), "cloudflare, fern, stainless"); +} diff --git a/src/normalize/walk.rs b/src/normalize/walk.rs index 8f5cda501..034e4aea1 100644 --- a/src/normalize/walk.rs +++ b/src/normalize/walk.rs @@ -9,7 +9,7 @@ use serde_json::{Map, Value}; -use super::{Options, Report}; +use super::{Change, Native, Options, Report}; /// A kind of object that is read. #[derive(Clone, Copy)] @@ -86,7 +86,7 @@ pub(super) fn walk(document: &mut Value, options: &Options) -> Report { } struct Walk<'o> { - options: &'o Options, + options: &'o Options<'o>, report: Report, } @@ -142,20 +142,50 @@ impl Walk<'_> { .keys() .filter(|key| key.starts_with("x-")) .filter_map(|key| { - let normalize = match kind { - Kind::Operation => self.options.sources.definition(key), - Kind::Parameter => self.options.sources.parameter(key), - Kind::Schema => self.options.sources.schema(key), + let normalizer = match kind { + Kind::Operation => self.options.source.definition(key), + Kind::Parameter => self.options.source.parameter(key), + Kind::Schema => self.options.source.schema(key), Kind::Document | Kind::Components | Kind::PathItem | Kind::Alias | Kind::Body | Kind::Media => None, }; - normalize.map(|normalize| (key.clone(), normalize)) + normalizer.map(|normalizer| (key.clone(), normalizer)) }) .collect(); - for (original, normalize) in normalizers { - if let Some(change) = normalize.normalize(members, &original, self.options) { - self.report.record(&original, change); + for (original, normalizer) in normalizers { + let Some(Native { + key: native, + value, + change, + }) = members.get(&original).and_then(|value| normalizer.normalize(value)) + else { + continue; + }; + + // The native value wins when both are present. Keeping + // compatibility keys makes this object already normalized; + // otherwise the old key is removed without touching the native one. + if members.contains_key(native) { + if !self.options.leave_old_extensions { + members.shift_remove(&original); + self.report.record(&original, Change::plain()); + } + continue; } + + let at = members + .keys() + .position(|key| *key == original) + .expect("the compatibility key was found in its container"); + // The value is only copied when the old key keeps it too. + let removed = if self.options.leave_old_extensions { + None + } else { + members.shift_remove(&original) + }; + let value = value.or(removed).unwrap_or_else(|| members[&original].clone()); + members.shift_insert(at, native.to_owned(), value); + self.report.record(&original, change); } } } From 692d879ce312aaa05441d5e8e775f07a11bf8940 Mon Sep 17 00:00:00 2001 From: Sri Krishna <7254698+srikrsna@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:04:19 +0530 Subject: [PATCH 2/2] Move to bin --- src/bin/forge/normalize.rs | 26 ++--- .../mod.rs => bin/forge/normalize/json.rs} | 103 ++++++------------ .../forge}/normalize/vendor/cloudflare.rs | 6 +- src/{ => bin/forge}/normalize/vendor/fern.rs | 6 +- src/{ => bin/forge}/normalize/vendor/mod.rs | 66 ++--------- .../forge}/normalize/vendor/stainless.rs | 6 +- src/{ => bin/forge}/normalize/vendor/tests.rs | 28 ++--- src/{ => bin/forge}/normalize/walk.rs | 14 ++- src/lib.rs | 2 - 9 files changed, 80 insertions(+), 177 deletions(-) rename src/{normalize/mod.rs => bin/forge/normalize/json.rs} (60%) rename src/{ => bin/forge}/normalize/vendor/cloudflare.rs (76%) rename src/{ => bin/forge}/normalize/vendor/fern.rs (91%) rename src/{ => bin/forge}/normalize/vendor/mod.rs (74%) rename src/{ => bin/forge}/normalize/vendor/stainless.rs (78%) rename src/{ => bin/forge}/normalize/vendor/tests.rs (80%) rename src/{ => bin/forge}/normalize/walk.rs (94%) diff --git a/src/bin/forge/normalize.rs b/src/bin/forge/normalize.rs index 0f030c366..277f62477 100644 --- a/src/bin/forge/normalize.rs +++ b/src/bin/forge/normalize.rs @@ -1,5 +1,10 @@ //! `forge normalize`, which rewrites the extensions other tools' metadata is -//! written with into the native ones, in the files it is given. +//! written with into the native ones, in the files it is given. How one +//! document is rewritten is in [`json`]. + +mod json; +mod vendor; +mod walk; use std::collections::HashSet; use std::fs; @@ -7,7 +12,9 @@ use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use forge::Err; -use forge::normalize::{Normalized, Options, Report, Source, normalize, sources}; + +use json::{Normalized, Options, Report, normalize}; +use vendor::Source; /// What `forge normalize` is given, which says what it does in `main.rs`. #[derive(clap::Args)] @@ -59,19 +66,9 @@ struct Say { } pub fn run(args: Args) -> Result<(), Err> { - let mut selected: Vec<&dyn Source> = Vec::new(); - for source in args.from { - if !selected.iter().any(|chosen| chosen.name() == source.name()) { - selected.push(source); - } - } let options = Options { leave_old_extensions: args.leave_old_extensions, - source: if selected.is_empty() { - &sources() - } else { - &selected.as_slice() - }, + sources: if args.from.is_empty() { vendor::ALL } else { &args.from }, }; let say = Say { check: args.check, @@ -142,8 +139,9 @@ pub fn run(args: Args) -> Result<(), Err> { Ok(()) } +/// Returns the compatibility source called `name`. fn source(name: &str) -> Result<&'static dyn Source, String> { - forge::normalize::source(name) + vendor::named(name) .ok_or_else(|| format!("unknown compatibility source {name:?}; expected cloudflare, fern, or stainless")) } diff --git a/src/normalize/mod.rs b/src/bin/forge/normalize/json.rs similarity index 60% rename from src/normalize/mod.rs rename to src/bin/forge/normalize/json.rs index 036e6d626..cc65a0f35 100644 --- a/src/normalize/mod.rs +++ b/src/bin/forge/normalize/json.rs @@ -1,85 +1,62 @@ -//! Rewrites the extensions other tools' metadata is written with into the -//! native `x-forge-*` ones, in a JSON document. +//! Normalizes one JSON document. //! -//! The key a tool writes is found with the sources of [`vendor`], at the kinds -//! of OpenAPI object where its native replacement belongs. A key elsewhere is -//! left alone, since rewriting it would give it a meaning it did not have. +//! The key a tool writes is looked up in the sources of [`super::vendor`], at +//! the kinds of OpenAPI object where its native replacement belongs. A key +//! elsewhere is left alone, since rewriting it would give it a meaning it did +//! not have. //! //! A native key is never replaced. When a document says the same thing with a //! native key and another, the native one is kept. //! //! The document is read with `serde_json` and written again, in the order its //! keys were in, with the indentation of its first indented line. That is -//! simple, and it is not a minimal edit: what is lost is listed in the package -//! README under "Known limitations". - -mod vendor; -mod walk; +//! simple, but it is not a minimal edit. The package README lists what is lost +//! under "Known limitations". use std::collections::BTreeMap; -use std::fmt; use serde::Serialize; use serde_json::Value; use serde_json::ser::PrettyFormatter; -use vendor::Detail; -pub use vendor::{Change, Native, Normalize, Source}; - -/// Every compatibility source. As a slice, they are a [`Source`] together. -pub fn sources() -> &'static [&'static dyn Source] { - vendor::ALL -} - -/// Returns the compatibility source called `name`, if there is one. -pub fn source(name: &str) -> Option<&'static dyn Source> { - vendor::ALL.iter().find(|source| source.name() == name).copied() -} +use super::vendor::{Change, Detail, Source}; +use super::walk; /// What [`normalize`] does. -#[derive(Clone, Copy, Debug)] -pub struct Options<'a> { +#[derive(Clone, Copy)] +pub(super) struct Options<'a> { /// Keeps each key that is rewritten, beside the native key that replaces /// it, for what still reads the old one. - pub leave_old_extensions: bool, - /// The compatibility source to rewrite, which may be several combined. - pub source: &'a dyn Source, -} - -impl Default for Options<'_> { - fn default() -> Self { - Options { - leave_old_extensions: false, - source: &vendor::ALL, - } - } + pub(super) leave_old_extensions: bool, + /// The compatibility sources whose keys are rewritten. + pub(super) sources: &'a [&'static dyn Source], } /// What [`normalize`] did. #[derive(Debug, Default)] -pub struct Report { - /// How many keys were rewritten, or removed for a native key that - /// already said it, by the name of the key. - pub migrated: BTreeMap, +pub(super) struct Report { + /// How many times each key was rewritten, or removed because a native key + /// already said the same thing. + pub(super) migrated: BTreeMap, details: BTreeMap, } impl Report { /// Returns whether the document is different from what it was. - pub fn changed(&self) -> bool { + pub(super) fn changed(&self) -> bool { !self.migrated.is_empty() } /// Returns the additional descriptions of changes, and how many times /// each happened. - pub fn details(&self) -> impl Iterator + '_ { + pub(super) fn details(&self) -> impl Iterator + '_ { self.details.iter().map(|(detail, count)| { let description = if *count == 1 { detail.one } else { detail.many }; (*count, description) }) } - fn record(&mut self, original: &str, change: Change) { + pub(super) fn record(&mut self, original: &str, change: Change) { *self.migrated.entry(original.to_owned()).or_insert(0) += 1; if let Some(detail) = change.detail { *self.details.entry(detail).or_insert(0) += 1; @@ -89,48 +66,30 @@ impl Report { /// A document after [`normalize`]. #[derive(Debug)] -pub struct Normalized { +pub(super) struct Normalized { /// The document, if it is different from what was read. - pub json: Option>, + pub(super) json: Option>, /// What was done to it. - pub report: Report, -} - -/// Why a document could not be normalized. -#[derive(Debug)] -pub struct Error(String); - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -impl std::error::Error for Error {} - -impl From for Error { - fn from(err: serde_json::Error) -> Error { - Error(err.to_string()) - } + pub(super) report: Report, } /// Rewrites the keys of the document `json` that a native key replaces. -pub fn normalize(json: &[u8], options: &Options) -> Result { +pub(super) fn normalize(json: &[u8], options: &Options) -> Result { let mut document: Value = serde_json::from_slice(json)?; let report = walk::walk(&mut document, options); if !report.changed() { return Ok(Normalized { json: None, report }); } Ok(Normalized { - json: Some(write(&document, json)), + json: Some(encode(&document, json)), report, }) } -/// Writes `document` as `original` was written, as far as it is known: with +/// Encodes `document` as `original` was written, as far as that is known: with /// its indentation, which is that of the first line that is indented, on one /// line if it was, and with a newline at the end if it had one. -fn write(document: &Value, original: &[u8]) -> Vec { +fn encode(document: &Value, original: &[u8]) -> Vec { let mut out = Vec::with_capacity(original.len() + original.len() / 16); match indentation(original) { Some(indent) => { @@ -174,7 +133,7 @@ mod tests { fn a_document_on_one_line_is_written_on_one_line() { assert_eq!(indentation(br#"{"a":1}"#), None); assert_eq!( - write(&serde_json::json!({"a": 1, "b": [1, 2]}), br#"{"a":1}"#), + encode(&serde_json::json!({"a": 1, "b": [1, 2]}), br#"{"a":1}"#), br#"{"a":1,"b":[1,2]}"# ); } @@ -192,7 +151,7 @@ mod tests { #[test] fn the_newline_at_the_end_is_kept_if_there_was_one() { let document = serde_json::json!({"a": 1}); - assert!(write(&document, b"{\n \"a\": 1\n}\n").ends_with(b"}\n")); - assert!(write(&document, b"{\n \"a\": 1\n}").ends_with(b"}")); + assert!(encode(&document, b"{\n \"a\": 1\n}\n").ends_with(b"}\n")); + assert!(encode(&document, b"{\n \"a\": 1\n}").ends_with(b"}")); } } diff --git a/src/normalize/vendor/cloudflare.rs b/src/bin/forge/normalize/vendor/cloudflare.rs similarity index 76% rename from src/normalize/vendor/cloudflare.rs rename to src/bin/forge/normalize/vendor/cloudflare.rs index d6876d404..de7df8c9d 100644 --- a/src/normalize/vendor/cloudflare.rs +++ b/src/bin/forge/normalize/vendor/cloudflare.rs @@ -1,14 +1,12 @@ //! The adapter for Cloudflare's compatibility extensions. -use std::borrow::Cow; - use super::{Normalize, Source}; pub(super) struct Cloudflare; impl Source for Cloudflare { - fn name(&self) -> Cow<'static, str> { - Cow::Borrowed("cloudflare") + fn name(&self) -> &'static str { + "cloudflare" } fn schema(&self, key: &str) -> Option<&dyn Normalize> { diff --git a/src/normalize/vendor/fern.rs b/src/bin/forge/normalize/vendor/fern.rs similarity index 91% rename from src/normalize/vendor/fern.rs rename to src/bin/forge/normalize/vendor/fern.rs index 6b3810b60..091e50620 100644 --- a/src/normalize/vendor/fern.rs +++ b/src/bin/forge/normalize/vendor/fern.rs @@ -1,14 +1,12 @@ //! The adapter for Fern's `x-fern-*` extensions. -use std::borrow::Cow; - use super::{Group, Normalize, Source}; pub(super) struct Fern; impl Source for Fern { - fn name(&self) -> Cow<'static, str> { - Cow::Borrowed("fern") + fn name(&self) -> &'static str { + "fern" } fn definition(&self, key: &str) -> Option<&dyn Normalize> { diff --git a/src/normalize/vendor/mod.rs b/src/bin/forge/normalize/vendor/mod.rs similarity index 74% rename from src/normalize/vendor/mod.rs rename to src/bin/forge/normalize/vendor/mod.rs index 58db3fe70..c3710f58c 100644 --- a/src/normalize/vendor/mod.rs +++ b/src/bin/forge/normalize/vendor/mod.rs @@ -1,4 +1,4 @@ -//! Compatibility-source adapters for [`super::normalize`]. +//! Compatibility-source adapters for [`super::json::normalize`]. //! //! A [`Source`] names the normalizers that apply to operations, parameters, //! and schemas. A key is listed only at the OpenAPI object kind where Forge @@ -17,18 +17,20 @@ mod stainless; #[cfg(test)] mod tests; -use std::borrow::Cow; -use std::fmt; - use serde_json::Value; /// Every available compatibility source. pub(super) static ALL: &[&dyn Source] = &[&cloudflare::Cloudflare, &fern::Fern, &stainless::Stainless]; +/// Returns the compatibility source called `name`, if there is one. +pub(super) fn named(name: &str) -> Option<&'static dyn Source> { + ALL.iter().find(|source| source.name() == name).copied() +} + /// A source of compatibility extensions. -pub trait Source: Sync { +pub(super) trait Source: Sync { /// The name accepted by `--from`. - fn name(&self) -> Cow<'static, str>; + fn name(&self) -> &'static str; /// The normalizer for `key` on operations and aliases, if there is one. fn definition(&self, _key: &str) -> Option<&dyn Normalize> { @@ -47,14 +49,14 @@ pub trait Source: Sync { } /// Normalizes one compatibility extension into its [`Native`] form. -pub trait Normalize: Sync { +pub(super) trait Normalize: Sync { /// The native form of an extension whose value is `value`, or none when /// it has no native form and the extension is left alone. fn normalize(&self, value: &Value) -> Option; } /// An extension in its native form. -pub struct Native { +pub(super) struct Native { /// The native key that replaces the compatibility key. pub(super) key: &'static str, /// The value in its native form, or none when it is kept as it is. @@ -75,7 +77,7 @@ impl Native { /// A change made by one normalizer. #[derive(Clone, Copy, Debug)] -pub struct Change { +pub(super) struct Change { pub(super) detail: Option, } @@ -151,49 +153,3 @@ fn dotted(names: &[Value]) -> Option { } Some(path) } - -/// Many sources combined into one: the first that has a normalizer for a key -/// wins. -impl Source for [S] { - /// The names of the sources, separated by commas. - fn name(&self) -> Cow<'static, str> { - Cow::Owned(self.iter().map(Source::name).collect::>().join(", ")) - } - - fn definition(&self, key: &str) -> Option<&dyn Normalize> { - self.iter().find_map(|source| source.definition(key)) - } - - fn parameter(&self, key: &str) -> Option<&dyn Normalize> { - self.iter().find_map(|source| source.parameter(key)) - } - - fn schema(&self, key: &str) -> Option<&dyn Normalize> { - self.iter().find_map(|source| source.schema(key)) - } -} - -/// So that `[&dyn Source]`, a slice of different sources, is a `Source` too. -impl Source for &S { - fn name(&self) -> Cow<'static, str> { - (**self).name() - } - - fn definition(&self, key: &str) -> Option<&dyn Normalize> { - (**self).definition(key) - } - - fn parameter(&self, key: &str) -> Option<&dyn Normalize> { - (**self).parameter(key) - } - - fn schema(&self, key: &str) -> Option<&dyn Normalize> { - (**self).schema(key) - } -} - -impl fmt::Debug for dyn Source + '_ { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.name()) - } -} diff --git a/src/normalize/vendor/stainless.rs b/src/bin/forge/normalize/vendor/stainless.rs similarity index 78% rename from src/normalize/vendor/stainless.rs rename to src/bin/forge/normalize/vendor/stainless.rs index 354c62de6..f41f12550 100644 --- a/src/normalize/vendor/stainless.rs +++ b/src/bin/forge/normalize/vendor/stainless.rs @@ -1,14 +1,12 @@ //! The adapter for Stainless's `x-stainless-*` extensions. -use std::borrow::Cow; - use super::{BoolOrTargets, Normalize, Source}; pub(super) struct Stainless; impl Source for Stainless { - fn name(&self) -> Cow<'static, str> { - Cow::Borrowed("stainless") + fn name(&self) -> &'static str { + "stainless" } fn schema(&self, key: &str) -> Option<&dyn Normalize> { diff --git a/src/normalize/vendor/tests.rs b/src/bin/forge/normalize/vendor/tests.rs similarity index 80% rename from src/normalize/vendor/tests.rs rename to src/bin/forge/normalize/vendor/tests.rs index 829da9e96..560b42c49 100644 --- a/src/normalize/vendor/tests.rs +++ b/src/bin/forge/normalize/vendor/tests.rs @@ -23,10 +23,6 @@ const KEYS: [(&str, Level, &str); 9] = [ ("stainless", Level::Schema, "x-stainless-skip"), ]; -fn named(name: &str) -> &'static dyn Source { - crate::normalize::source(name).unwrap() -} - fn read<'a>(source: &'a dyn Source, level: Level, key: &str) -> Option<&'a dyn Normalize> { match level { Level::Definition => source.definition(key), @@ -35,12 +31,17 @@ fn read<'a>(source: &'a dyn Source, level: Level, key: &str) -> Option<&'a dyn N } } +/// Reads `key` from every source, as the walk does when all are selected. +fn read_all(level: Level, key: &str) -> Option<&'static dyn Normalize> { + ALL.iter().find_map(|source| read(*source, level, key)) +} + #[test] fn a_key_is_read_only_at_its_level() { for (name, level, key) in KEYS { for other in [Level::Definition, Level::Parameter, Level::Schema] { assert_eq!( - read(named(name), other, key).is_some(), + read(named(name).unwrap(), other, key).is_some(), other == level, "{key} at {other:?}" ); @@ -65,14 +66,14 @@ fn a_key_is_read_only_when_its_source_is_selected() { #[test] fn every_adapter_is_in_all_sources() { for (name, level, key) in KEYS { - assert!(read(&ALL, level, key).is_some(), "{name}: {key}"); + assert!(read_all(level, key).is_some(), "{name}: {key}"); } } #[test] fn every_normalizer_writes_a_native_key() { for (_, level, original) in KEYS { - let normalizer = read(&ALL, level, original).unwrap(); + let normalizer = read_all(level, original).unwrap(); let native = normalizer.normalize(&Value::Bool(true)); assert!( @@ -85,7 +86,7 @@ fn every_normalizer_writes_a_native_key() { #[test] fn only_the_stainless_skip_may_have_no_native_value() { for (_, level, original) in KEYS { - let normalizer = read(&ALL, level, original).unwrap(); + let normalizer = read_all(level, original).unwrap(); assert_eq!( normalizer.normalize(&serde_json::json!(["terraform"])).is_none(), @@ -107,13 +108,8 @@ fn the_other_extensions_of_these_tools_are_not_read() { "x-forge-skip", "x-forge-method-name", ] { - assert!(ALL.definition(key).is_none(), "{key}"); - assert!(ALL.parameter(key).is_none(), "{key}"); - assert!(ALL.schema(key).is_none(), "{key}"); + for level in [Level::Definition, Level::Parameter, Level::Schema] { + assert!(read_all(level, key).is_none(), "{key} at {level:?}"); + } } } - -#[test] -fn sources_together_are_named_by_their_names_separated_by_commas() { - assert_eq!(ALL.name(), "cloudflare, fern, stainless"); -} diff --git a/src/normalize/walk.rs b/src/bin/forge/normalize/walk.rs similarity index 94% rename from src/normalize/walk.rs rename to src/bin/forge/normalize/walk.rs index 034e4aea1..e09af99ba 100644 --- a/src/normalize/walk.rs +++ b/src/bin/forge/normalize/walk.rs @@ -9,7 +9,8 @@ use serde_json::{Map, Value}; -use super::{Change, Native, Options, Report}; +use super::json::{Options, Report}; +use super::vendor::{Change, Native}; /// A kind of object that is read. #[derive(Clone, Copy)] @@ -142,12 +143,13 @@ impl Walk<'_> { .keys() .filter(|key| key.starts_with("x-")) .filter_map(|key| { - let normalizer = match kind { - Kind::Operation => self.options.source.definition(key), - Kind::Parameter => self.options.source.parameter(key), - Kind::Schema => self.options.source.schema(key), + // The first source with a normalizer for the key wins. + let normalizer = self.options.sources.iter().find_map(|source| match kind { + Kind::Operation => source.definition(key), + Kind::Parameter => source.parameter(key), + Kind::Schema => source.schema(key), Kind::Document | Kind::Components | Kind::PathItem | Kind::Alias | Kind::Body | Kind::Media => None, - }; + }); normalizer.map(|normalizer| (key.clone(), normalizer)) }) .collect(); diff --git a/src/lib.rs b/src/lib.rs index f9ba83e9f..b609d40cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,6 @@ mod json; mod link; mod method; mod name; -#[cfg(feature = "cli")] -pub mod normalize; mod plugin; mod types; pub use json::Json;