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    /// A boolean-typed single-value (`const`) or closed set (`enum`). JSON
73    /// Schema `const: true` lowers here with one value; a future
74    /// `enum: [true, false]` would carry both. See issue #107.
75    EnumBool(EnumBoolType),
76    /// A number-typed (non-integer) single-value (`const`) or closed set.
77    /// JSON Schema `const: 1.5` lowers here. Integer consts/enums stay on
78    /// [`TypeDef::EnumInt`]; this variant covers `number`-typed literals.
79    EnumNumber(EnumNumberType),
80    Union(UnionType),
81    /// JSON's `null` as a unit type. The canonical singleton lives in
82    /// [`crate::Ir::types`] under id [`NULL_ID`]; `T | null` is expressed
83    /// as a [`UnionType`] whose variants list contains a `Null` reference
84    /// (canonicalized to last). See issue #107.
85    Null,
86    /// The JSON Schema "any" schema — an empty/freeform schema (`{}`) or the
87    /// boolean schema `true`. It validates *any* instance (object, array,
88    /// string, number, boolean, or null). Per JSON Schema 2020-12 §4.3.2, `{}`
89    /// and `true` are equivalent. Distinct from an [`ObjectType`] with
90    /// permissive `additionalProperties` (`{"type":"object"}`), which validates
91    /// objects only — collapsing the two would reject otherwise-valid
92    /// non-object instances.
93    Any,
94}
95
96/// Canonical pool id for the [`TypeDef::Null`] singleton. See issue #107.
97pub const NULL_ID: &str = "null";
98
99// ---- primitives ----
100
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct PrimitiveType {
103    pub kind: PrimitiveKind,
104    pub constraints: PrimitiveConstraints,
105}
106
107/// JSON Schema's `type` keyword values, minus the variants that have
108/// their own IR shapes (`object` / `array` / `null`). The `format`
109/// keyword and any width/semantic refinement (`int32` / `int64` /
110/// `float` / `double` / `date` / `uuid` / `byte` / `decimal` / etc.)
111/// land on [`PrimitiveConstraints::format_extension`] verbatim.
112/// Plugins decide whether to produce a richer target-language type
113/// based on the format string. This keeps the IR uniform and
114/// orthogonal — adding new formats never requires an IR/WIT/bindgen
115/// roundtrip.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(rename_all = "kebab-case")]
118pub enum PrimitiveKind {
119    /// JSON Schema `type: "string"`.
120    String,
121    /// JSON Schema `type: "integer"`. Width refinements
122    /// (`int32` / `int64`) live in `format_extension`.
123    Integer,
124    /// JSON Schema `type: "number"`. Width refinements
125    /// (`float` / `double`) and `decimal` live in `format_extension`.
126    Number,
127    /// JSON Schema `type: "boolean"`.
128    Bool,
129}
130
131#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
132pub struct PrimitiveConstraints {
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub minimum: Option<ValueRef>,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub maximum: Option<ValueRef>,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub exclusive_minimum: Option<ValueRef>,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub exclusive_maximum: Option<ValueRef>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub multiple_of: Option<ValueRef>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub min_length: Option<u64>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub max_length: Option<u64>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub pattern: Option<String>,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub format_extension: Option<String>,
151    /// JSON Schema 2020-12 `contentEncoding` (e.g. `base64`,
152    /// `base32`). Describes how the string value is encoded; the
153    /// decoded payload may have its own media type and schema.
154    /// String-only — populated for `PrimitiveKind::String` schemas.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub content_encoding: Option<String>,
157    /// JSON Schema 2020-12 `contentMediaType` (e.g. `image/png`,
158    /// `application/json`). The media type of the decoded payload.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub content_media_type: Option<String>,
161    /// OAS 3.2 / JSON Schema 2020-12 `contentSchema` — schema for the
162    /// decoded payload (after applying `contentEncoding`). Carries a
163    /// `TypeRef` into [`crate::Ir::types`] so generators that decode
164    /// `contentEncoding` content can validate / shape it.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub content_schema: Option<TypeRef>,
167}
168
169// ---- arrays ----
170
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub struct ArrayType {
173    pub items: TypeRef,
174    pub constraints: ArrayConstraints,
175}
176
177#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
178pub struct ArrayConstraints {
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub min_items: Option<u64>,
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub max_items: Option<u64>,
183    #[serde(default)]
184    pub unique_items: bool,
185}
186
187// ---- objects ----
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190pub struct ObjectType {
191    pub properties: Vec<Property>,
192    /// JSON Schema `patternProperties` — each entry pairs an ECMA-262
193    /// regex against the schema that property *values* whose name
194    /// matches the regex must satisfy. Orthogonal to `properties`
195    /// (which match by exact name) and to `additional_properties`
196    /// (the fallback for names that match neither). Empty when the
197    /// keyword is absent.
198    #[serde(default, skip_serializing_if = "Vec::is_empty")]
199    pub pattern_properties: Vec<PatternProperty>,
200    pub additional_properties: AdditionalProperties,
201    /// JSON Schema `propertyNames` — a schema every property *name* in
202    /// the object must validate against. Names are always strings, so
203    /// this points at a string-shaped type (typically carrying a
204    /// `pattern` / `min_length` / `max_length` constraint). `None`
205    /// when the keyword is absent.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub property_names: Option<TypeRef>,
208    pub constraints: ObjectConstraints,
209}
210
211/// One entry of JSON Schema `patternProperties`: a property-name regex
212/// paired with the schema that matching property values must satisfy.
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct PatternProperty {
215    /// The raw ECMA-262 regular expression key. Generators targeting a
216    /// non-ECMA regex engine translate it, the same way they do for
217    /// [`PrimitiveConstraints::pattern`].
218    pub pattern: String,
219    /// Schema that values under a matching property name must satisfy.
220    #[serde(rename = "type")]
221    pub r#type: TypeRef,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225pub struct Property {
226    pub name: String,
227    #[serde(rename = "type")]
228    pub r#type: TypeRef,
229    /// `true` when the spec lists this property name in the parent
230    /// schema's `required` array. Moved here from
231    /// `ObjectType.required` for a uniform spelling with
232    /// `Parameter.required`.
233    #[serde(default)]
234    pub required: bool,
235    /// JSON Schema `title` — short human label.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub title: Option<String>,
238    /// JSON Schema / OAS `description` (CommonMark).
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub description: Option<String>,
241    /// JSON Schema 2020-12 `deprecated`.
242    #[serde(default)]
243    pub deprecated: bool,
244    #[serde(default)]
245    pub read_only: bool,
246    #[serde(default)]
247    pub write_only: bool,
248    /// Per-schema `externalDocs` (OAS Schema Object).
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub external_docs: Option<crate::ExternalDocs>,
251    /// JSON Schema `default` for the property. `ValueRef` indexes
252    /// into [`crate::Ir::values`]; compound defaults are pooled there.
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub default: Option<ValueRef>,
255    /// OAS `example` / `examples` for this property. Named entries;
256    /// 3.0 `example: <literal>` lands under the synthetic key
257    /// `"_default"`.
258    #[serde(default, skip_serializing_if = "Vec::is_empty")]
259    pub examples: Vec<(String, crate::Example)>,
260    /// `x-*` extensions declared on the property's schema. Each entry
261    /// pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
262    /// compound values are pooled there.
263    #[serde(default, skip_serializing_if = "Vec::is_empty")]
264    pub extensions: Vec<(String, ValueRef)>,
265}
266
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268#[serde(tag = "kind", rename_all = "kebab-case")]
269pub enum AdditionalProperties {
270    Forbidden,
271    Any,
272    Typed { r#type: TypeRef },
273}
274
275#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
276pub struct ObjectConstraints {
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub min_properties: Option<u64>,
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub max_properties: Option<u64>,
281}
282
283// ---- enums ----
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct EnumStringType {
287    pub values: Vec<EnumStringValue>,
288}
289
290/// One value in a string-typed enum. OAS / JSON Schema does not define
291/// per-value documentation, so this is a bare value. Per-value docs
292/// would have to come from a vendor extension and are out of scope.
293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
294pub struct EnumStringValue {
295    pub value: String,
296}
297
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub struct EnumIntType {
300    pub values: Vec<EnumIntValue>,
301    pub kind: IntKind,
302}
303
304/// One value in an integer-typed enum. OAS / JSON Schema does not
305/// define per-value documentation; see [`EnumStringValue`].
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct EnumIntValue {
308    pub value: i64,
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
312#[serde(rename_all = "kebab-case")]
313pub enum IntKind {
314    Int32,
315    Int64,
316}
317
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
319pub struct EnumBoolType {
320    pub values: Vec<EnumBoolValue>,
321}
322
323/// One value in a boolean-typed enum. OAS / JSON Schema does not define
324/// per-value documentation; see [`EnumStringValue`].
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326pub struct EnumBoolValue {
327    pub value: bool,
328}
329
330#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
331pub struct EnumNumberType {
332    pub values: Vec<EnumNumberValue>,
333    pub kind: NumberKind,
334}
335
336/// One value in a number-typed enum. OAS / JSON Schema does not define
337/// per-value documentation; see [`EnumStringValue`].
338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
339pub struct EnumNumberValue {
340    pub value: f64,
341}
342
343/// Width refinement for a number-typed literal, mirroring [`IntKind`].
344/// `format: float` selects [`NumberKind::Float`]; everything else
345/// (including `format: double` and an absent format) is [`NumberKind::Double`].
346#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
347#[serde(rename_all = "kebab-case")]
348pub enum NumberKind {
349    Float,
350    Double,
351}
352
353// ---- unions ----
354
355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
356pub struct UnionType {
357    pub variants: Vec<UnionVariant>,
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub discriminator: Option<Discriminator>,
360    pub kind: UnionKind,
361}
362
363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
364pub struct UnionVariant {
365    #[serde(rename = "type")]
366    pub r#type: TypeRef,
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub tag: Option<String>,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
372#[serde(rename_all = "kebab-case")]
373pub enum UnionKind {
374    OneOf,
375    AnyOf,
376}
377
378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379pub struct Discriminator {
380    pub property_name: String,
381    pub mapping: Vec<(String, TypeRef)>,
382    /// `x-*` extensions declared on the discriminator object. Each
383    /// entry pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
384    /// compound extension values are pooled there.
385    #[serde(default, skip_serializing_if = "Vec::is_empty")]
386    pub extensions: Vec<(String, ValueRef)>,
387}