Skip to main content

forge_ir/
types.rs

1//! Type-system pieces of the IR.
2//!
3//! Every named type lives in [`crate::Ir::types`] keyed by its sanitized [`NamedType::id`].
4//! [`TypeRef`] is just that string. This makes recursion trivial across the WIT
5//! boundary (no recursive records) and keeps the structure flat.
6
7use serde::{Deserialize, Serialize};
8
9use crate::diagnostic::SpecLocation;
10use crate::value::ValueRef;
11
12/// String id into [`crate::Ir::types`].
13pub type TypeRef = String;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct NamedType {
17    pub id: String,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub original_name: Option<String>,
20    /// JSON Schema `title` — short human label.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub title: Option<String>,
23    /// JSON Schema / OAS `description` (CommonMark).
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub description: Option<String>,
26    /// JSON Schema 2020-12 `deprecated`.
27    #[serde(default, skip_serializing_if = "crate::is_false")]
28    pub deprecated: bool,
29    /// JSON Schema `readOnly` at the schema level. Generators that
30    /// distinguish request from response shapes can opt to drop
31    /// `read_only` types from their request surface.
32    #[serde(default, skip_serializing_if = "crate::is_false")]
33    pub read_only: bool,
34    /// JSON Schema `writeOnly` at the schema level. Generators that
35    /// distinguish request from response shapes can opt to drop
36    /// `write_only` types from their response surface.
37    #[serde(default, skip_serializing_if = "crate::is_false")]
38    pub write_only: bool,
39    /// Per-schema `externalDocs` (OAS Schema Object).
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub external_docs: Option<crate::ExternalDocs>,
42    /// JSON Schema `default` at the schema level. `ValueRef` indexes
43    /// into [`crate::Ir::values`]; compound defaults are pooled there.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub default: Option<ValueRef>,
46    /// OAS `example` / `examples` on the schema. Named entries; 3.0
47    /// `example: <literal>` lands under the synthetic key `"_default"`.
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub examples: Vec<(String, crate::Example)>,
50    /// OAS `xml` block — name override, namespace, prefix, attribute
51    /// placement, array wrapping. None unless the spec declared one.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub xml: Option<crate::XmlObject>,
54    pub definition: TypeDef,
55    /// `x-*` extensions declared on the schema. Each entry pairs a key
56    /// with a [`ValueRef`] into [`crate::Ir::values`]; compound values
57    /// are pooled there.
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub extensions: Vec<(String, ValueRef)>,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub location: Option<SpecLocation>,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65#[serde(tag = "def", rename_all = "kebab-case")]
66pub enum TypeDef {
67    Primitive(PrimitiveType),
68    Object(ObjectType),
69    Array(ArrayType),
70    EnumString(EnumStringType),
71    EnumInt(EnumIntType),
72    Union(UnionType),
73    /// JSON's `null` as a unit type. The canonical singleton lives in
74    /// [`crate::Ir::types`] under id [`NULL_ID`]; `T | null` is expressed
75    /// as a [`UnionType`] whose variants list contains a `Null` reference
76    /// (canonicalized to last). See issue #107.
77    Null,
78    /// The JSON Schema "any" schema — an empty/freeform schema (`{}`) or the
79    /// boolean schema `true`. It validates *any* instance (object, array,
80    /// string, number, boolean, or null). Per JSON Schema 2020-12 §4.3.2, `{}`
81    /// and `true` are equivalent. Distinct from an [`ObjectType`] with
82    /// permissive `additionalProperties` (`{"type":"object"}`), which validates
83    /// objects only — collapsing the two would reject otherwise-valid
84    /// non-object instances.
85    Any,
86}
87
88/// Canonical pool id for the [`TypeDef::Null`] singleton. See issue #107.
89pub const NULL_ID: &str = "null";
90
91// ---- primitives ----
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub struct PrimitiveType {
95    pub kind: PrimitiveKind,
96    pub constraints: PrimitiveConstraints,
97}
98
99/// JSON Schema's `type` keyword values, minus the variants that have
100/// their own IR shapes (`object` / `array` / `null`). The `format`
101/// keyword and any width/semantic refinement (`int32` / `int64` /
102/// `float` / `double` / `date` / `uuid` / `byte` / `decimal` / etc.)
103/// land on [`PrimitiveConstraints::format_extension`] verbatim.
104/// Plugins decide whether to produce a richer target-language type
105/// based on the format string. This keeps the IR uniform and
106/// orthogonal — adding new formats never requires an IR/WIT/bindgen
107/// roundtrip.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
109#[serde(rename_all = "kebab-case")]
110pub enum PrimitiveKind {
111    /// JSON Schema `type: "string"`.
112    String,
113    /// JSON Schema `type: "integer"`. Width refinements
114    /// (`int32` / `int64`) live in `format_extension`.
115    Integer,
116    /// JSON Schema `type: "number"`. Width refinements
117    /// (`float` / `double`) and `decimal` live in `format_extension`.
118    Number,
119    /// JSON Schema `type: "boolean"`.
120    Bool,
121}
122
123#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
124pub struct PrimitiveConstraints {
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub minimum: Option<ValueRef>,
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub maximum: Option<ValueRef>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub exclusive_minimum: Option<ValueRef>,
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub exclusive_maximum: Option<ValueRef>,
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub multiple_of: Option<ValueRef>,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub min_length: Option<u64>,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub max_length: Option<u64>,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub pattern: Option<String>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub format_extension: Option<String>,
143    /// JSON Schema 2020-12 `contentEncoding` (e.g. `base64`,
144    /// `base32`). Describes how the string value is encoded; the
145    /// decoded payload may have its own media type and schema.
146    /// String-only — populated for `PrimitiveKind::String` schemas.
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub content_encoding: Option<String>,
149    /// JSON Schema 2020-12 `contentMediaType` (e.g. `image/png`,
150    /// `application/json`). The media type of the decoded payload.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub content_media_type: Option<String>,
153    /// OAS 3.2 / JSON Schema 2020-12 `contentSchema` — schema for the
154    /// decoded payload (after applying `contentEncoding`). Carries a
155    /// `TypeRef` into [`crate::Ir::types`] so generators that decode
156    /// `contentEncoding` content can validate / shape it.
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub content_schema: Option<TypeRef>,
159}
160
161// ---- arrays ----
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct ArrayType {
165    pub items: TypeRef,
166    pub constraints: ArrayConstraints,
167}
168
169#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
170pub struct ArrayConstraints {
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub min_items: Option<u64>,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub max_items: Option<u64>,
175    #[serde(default)]
176    pub unique_items: bool,
177}
178
179// ---- objects ----
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182pub struct ObjectType {
183    pub properties: Vec<Property>,
184    /// JSON Schema `patternProperties` — each entry pairs an ECMA-262
185    /// regex against the schema that property *values* whose name
186    /// matches the regex must satisfy. Orthogonal to `properties`
187    /// (which match by exact name) and to `additional_properties`
188    /// (the fallback for names that match neither). Empty when the
189    /// keyword is absent.
190    #[serde(default, skip_serializing_if = "Vec::is_empty")]
191    pub pattern_properties: Vec<PatternProperty>,
192    pub additional_properties: AdditionalProperties,
193    /// JSON Schema `propertyNames` — a schema every property *name* in
194    /// the object must validate against. Names are always strings, so
195    /// this points at a string-shaped type (typically carrying a
196    /// `pattern` / `min_length` / `max_length` constraint). `None`
197    /// when the keyword is absent.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub property_names: Option<TypeRef>,
200    pub constraints: ObjectConstraints,
201}
202
203/// One entry of JSON Schema `patternProperties`: a property-name regex
204/// paired with the schema that matching property values must satisfy.
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub struct PatternProperty {
207    /// The raw ECMA-262 regular expression key. Generators targeting a
208    /// non-ECMA regex engine translate it, the same way they do for
209    /// [`PrimitiveConstraints::pattern`].
210    pub pattern: String,
211    /// Schema that values under a matching property name must satisfy.
212    #[serde(rename = "type")]
213    pub r#type: TypeRef,
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct Property {
218    pub name: String,
219    #[serde(rename = "type")]
220    pub r#type: TypeRef,
221    /// `true` when the spec lists this property name in the parent
222    /// schema's `required` array. Moved here from
223    /// `ObjectType.required` for a uniform spelling with
224    /// `Parameter.required`.
225    #[serde(default)]
226    pub required: bool,
227    /// JSON Schema `title` — short human label.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub title: Option<String>,
230    /// JSON Schema / OAS `description` (CommonMark).
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub description: Option<String>,
233    /// JSON Schema 2020-12 `deprecated`.
234    #[serde(default)]
235    pub deprecated: bool,
236    #[serde(default)]
237    pub read_only: bool,
238    #[serde(default)]
239    pub write_only: bool,
240    /// Per-schema `externalDocs` (OAS Schema Object).
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub external_docs: Option<crate::ExternalDocs>,
243    /// JSON Schema `default` for the property. `ValueRef` indexes
244    /// into [`crate::Ir::values`]; compound defaults are pooled there.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub default: Option<ValueRef>,
247    /// OAS `example` / `examples` for this property. Named entries;
248    /// 3.0 `example: <literal>` lands under the synthetic key
249    /// `"_default"`.
250    #[serde(default, skip_serializing_if = "Vec::is_empty")]
251    pub examples: Vec<(String, crate::Example)>,
252    /// `x-*` extensions declared on the property's schema. Each entry
253    /// pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
254    /// compound values are pooled there.
255    #[serde(default, skip_serializing_if = "Vec::is_empty")]
256    pub extensions: Vec<(String, ValueRef)>,
257}
258
259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
260#[serde(tag = "kind", rename_all = "kebab-case")]
261pub enum AdditionalProperties {
262    Forbidden,
263    Any,
264    Typed { r#type: TypeRef },
265}
266
267#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
268pub struct ObjectConstraints {
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub min_properties: Option<u64>,
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub max_properties: Option<u64>,
273}
274
275// ---- enums ----
276
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
278pub struct EnumStringType {
279    pub values: Vec<EnumStringValue>,
280}
281
282/// One value in a string-typed enum. OAS / JSON Schema does not define
283/// per-value documentation, so this is a bare value. Per-value docs
284/// would have to come from a vendor extension and are out of scope.
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct EnumStringValue {
287    pub value: String,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub struct EnumIntType {
292    pub values: Vec<EnumIntValue>,
293    pub kind: IntKind,
294}
295
296/// One value in an integer-typed enum. OAS / JSON Schema does not
297/// define per-value documentation; see [`EnumStringValue`].
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub struct EnumIntValue {
300    pub value: i64,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
304#[serde(rename_all = "kebab-case")]
305pub enum IntKind {
306    Int32,
307    Int64,
308}
309
310// ---- unions ----
311
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
313pub struct UnionType {
314    pub variants: Vec<UnionVariant>,
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub discriminator: Option<Discriminator>,
317    pub kind: UnionKind,
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321pub struct UnionVariant {
322    #[serde(rename = "type")]
323    pub r#type: TypeRef,
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub tag: Option<String>,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
329#[serde(rename_all = "kebab-case")]
330pub enum UnionKind {
331    OneOf,
332    AnyOf,
333}
334
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336pub struct Discriminator {
337    pub property_name: String,
338    pub mapping: Vec<(String, TypeRef)>,
339    /// `x-*` extensions declared on the discriminator object. Each
340    /// entry pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
341    /// compound extension values are pooled there.
342    #[serde(default, skip_serializing_if = "Vec::is_empty")]
343    pub extensions: Vec<(String, ValueRef)>,
344}