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    pub additional_properties: AdditionalProperties,
185    pub constraints: ObjectConstraints,
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct Property {
190    pub name: String,
191    #[serde(rename = "type")]
192    pub r#type: TypeRef,
193    /// `true` when the spec lists this property name in the parent
194    /// schema's `required` array. Moved here from
195    /// `ObjectType.required` for a uniform spelling with
196    /// `Parameter.required`.
197    #[serde(default)]
198    pub required: bool,
199    /// JSON Schema `title` — short human label.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub title: Option<String>,
202    /// JSON Schema / OAS `description` (CommonMark).
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub description: Option<String>,
205    /// JSON Schema 2020-12 `deprecated`.
206    #[serde(default)]
207    pub deprecated: bool,
208    #[serde(default)]
209    pub read_only: bool,
210    #[serde(default)]
211    pub write_only: bool,
212    /// Per-schema `externalDocs` (OAS Schema Object).
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub external_docs: Option<crate::ExternalDocs>,
215    /// JSON Schema `default` for the property. `ValueRef` indexes
216    /// into [`crate::Ir::values`]; compound defaults are pooled there.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub default: Option<ValueRef>,
219    /// OAS `example` / `examples` for this property. Named entries;
220    /// 3.0 `example: <literal>` lands under the synthetic key
221    /// `"_default"`.
222    #[serde(default, skip_serializing_if = "Vec::is_empty")]
223    pub examples: Vec<(String, crate::Example)>,
224    /// `x-*` extensions declared on the property's schema. Each entry
225    /// pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
226    /// compound values are pooled there.
227    #[serde(default, skip_serializing_if = "Vec::is_empty")]
228    pub extensions: Vec<(String, ValueRef)>,
229}
230
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232#[serde(tag = "kind", rename_all = "kebab-case")]
233pub enum AdditionalProperties {
234    Forbidden,
235    Any,
236    Typed { r#type: TypeRef },
237}
238
239#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
240pub struct ObjectConstraints {
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub min_properties: Option<u64>,
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub max_properties: Option<u64>,
245}
246
247// ---- enums ----
248
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250pub struct EnumStringType {
251    pub values: Vec<EnumStringValue>,
252}
253
254/// One value in a string-typed enum. OAS / JSON Schema does not define
255/// per-value documentation, so this is a bare value. Per-value docs
256/// would have to come from a vendor extension and are out of scope.
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258pub struct EnumStringValue {
259    pub value: String,
260}
261
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263pub struct EnumIntType {
264    pub values: Vec<EnumIntValue>,
265    pub kind: IntKind,
266}
267
268/// One value in an integer-typed enum. OAS / JSON Schema does not
269/// define per-value documentation; see [`EnumStringValue`].
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct EnumIntValue {
272    pub value: i64,
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
276#[serde(rename_all = "kebab-case")]
277pub enum IntKind {
278    Int32,
279    Int64,
280}
281
282// ---- unions ----
283
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub struct UnionType {
286    pub variants: Vec<UnionVariant>,
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub discriminator: Option<Discriminator>,
289    pub kind: UnionKind,
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
293pub struct UnionVariant {
294    #[serde(rename = "type")]
295    pub r#type: TypeRef,
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub tag: Option<String>,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
301#[serde(rename_all = "kebab-case")]
302pub enum UnionKind {
303    OneOf,
304    AnyOf,
305}
306
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308pub struct Discriminator {
309    pub property_name: String,
310    pub mapping: Vec<(String, TypeRef)>,
311    /// `x-*` extensions declared on the discriminator object. Each
312    /// entry pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
313    /// compound extension values are pooled there.
314    #[serde(default, skip_serializing_if = "Vec::is_empty")]
315    pub extensions: Vec<(String, ValueRef)>,
316}