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    /// Long-form schema description (`description` in JSON Schema).
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub documentation: Option<String>,
23    /// Short human label (`title` in JSON Schema). Doc generators and IDE
24    /// hover surface it; populated verbatim, never derived.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub title: Option<String>,
27    /// JSON Schema `readOnly` at the schema level. Generators that
28    /// distinguish request from response shapes can opt to drop
29    /// `read_only` types from their request surface.
30    #[serde(default, skip_serializing_if = "is_false")]
31    pub read_only: bool,
32    /// JSON Schema `writeOnly` at the schema level. Generators that
33    /// distinguish request from response shapes can opt to drop
34    /// `write_only` types from their response surface.
35    #[serde(default, skip_serializing_if = "is_false")]
36    pub write_only: bool,
37    /// Per-schema `externalDocs` (OAS Schema Object).
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub external_docs: Option<crate::ExternalDocs>,
40    /// JSON Schema `default` at the schema level. `ValueRef` indexes
41    /// into [`crate::Ir::values`]; compound defaults are pooled there.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub default: Option<ValueRef>,
44    /// OAS `example` / `examples` on the schema. Named entries; 3.0
45    /// `example: <literal>` lands under the synthetic key `"_default"`.
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub examples: Vec<(String, crate::Example)>,
48    /// OAS `xml` block — name override, namespace, prefix, attribute
49    /// placement, array wrapping. None unless the spec declared one.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub xml: Option<crate::XmlObject>,
52    pub definition: TypeDef,
53    /// `x-*` extensions declared on the schema. Each entry pairs a key
54    /// with a [`ValueRef`] into [`crate::Ir::values`]; compound values
55    /// are pooled there.
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub extensions: Vec<(String, ValueRef)>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub location: Option<SpecLocation>,
60}
61
62fn is_false(b: &bool) -> bool {
63    !*b
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(tag = "def", rename_all = "kebab-case")]
68pub enum TypeDef {
69    Primitive(PrimitiveType),
70    Object(ObjectType),
71    Array(ArrayType),
72    EnumString(EnumStringType),
73    EnumInt(EnumIntType),
74    Union(UnionType),
75    /// JSON's `null` as a unit type. The canonical singleton lives in
76    /// [`crate::Ir::types`] under id [`NULL_ID`]; `T | null` is expressed
77    /// as a [`UnionType`] whose variants list contains a `Null` reference
78    /// (canonicalized to last). See issue #107.
79    Null,
80}
81
82/// Canonical pool id for the [`TypeDef::Null`] singleton. See issue #107.
83pub const NULL_ID: &str = "null";
84
85// ---- primitives ----
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct PrimitiveType {
89    pub kind: PrimitiveKind,
90    pub constraints: PrimitiveConstraints,
91}
92
93/// JSON Schema's `type` keyword values, minus the variants that have
94/// their own IR shapes (`object` / `array` / `null`). The `format`
95/// keyword and any width/semantic refinement (`int32` / `int64` /
96/// `float` / `double` / `date` / `uuid` / `byte` / `decimal` / etc.)
97/// land on [`PrimitiveConstraints::format_extension`] verbatim.
98/// Plugins decide whether to produce a richer target-language type
99/// based on the format string. This keeps the IR uniform and
100/// orthogonal — adding new formats never requires an IR/WIT/bindgen
101/// roundtrip.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
103#[serde(rename_all = "kebab-case")]
104pub enum PrimitiveKind {
105    /// JSON Schema `type: "string"`.
106    String,
107    /// JSON Schema `type: "integer"`. Width refinements
108    /// (`int32` / `int64`) live in `format_extension`.
109    Integer,
110    /// JSON Schema `type: "number"`. Width refinements
111    /// (`float` / `double`) and `decimal` live in `format_extension`.
112    Number,
113    /// JSON Schema `type: "boolean"`.
114    Bool,
115}
116
117#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
118pub struct PrimitiveConstraints {
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub minimum: Option<ValueRef>,
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub maximum: Option<ValueRef>,
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub exclusive_minimum: Option<ValueRef>,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub exclusive_maximum: Option<ValueRef>,
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub multiple_of: Option<ValueRef>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub min_length: Option<u64>,
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub max_length: Option<u64>,
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub pattern: Option<String>,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub format_extension: Option<String>,
137    /// JSON Schema 2020-12 `contentEncoding` (e.g. `base64`,
138    /// `base32`). Describes how the string value is encoded; the
139    /// decoded payload may have its own media type and schema.
140    /// String-only — populated for `PrimitiveKind::String` schemas.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub content_encoding: Option<String>,
143    /// JSON Schema 2020-12 `contentMediaType` (e.g. `image/png`,
144    /// `application/json`). The media type of the decoded payload.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub content_media_type: Option<String>,
147    /// OAS 3.2 / JSON Schema 2020-12 `contentSchema` — schema for the
148    /// decoded payload (after applying `contentEncoding`). Carries a
149    /// `TypeRef` into [`crate::Ir::types`] so generators that decode
150    /// `contentEncoding` content can validate / shape it.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub content_schema: Option<TypeRef>,
153}
154
155// ---- arrays ----
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct ArrayType {
159    pub items: TypeRef,
160    pub constraints: ArrayConstraints,
161}
162
163#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
164pub struct ArrayConstraints {
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub min_items: Option<u64>,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub max_items: Option<u64>,
169    #[serde(default)]
170    pub unique_items: bool,
171}
172
173// ---- objects ----
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct ObjectType {
177    pub properties: Vec<Property>,
178    pub additional_properties: AdditionalProperties,
179    pub constraints: ObjectConstraints,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct Property {
184    pub name: String,
185    #[serde(rename = "type")]
186    pub r#type: TypeRef,
187    /// `true` when the spec lists this property name in the parent
188    /// schema's `required` array. Moved here from
189    /// `ObjectType.required` for a uniform spelling with
190    /// `Parameter.required`.
191    #[serde(default)]
192    pub required: bool,
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub documentation: Option<String>,
195    #[serde(default)]
196    pub deprecated: bool,
197    #[serde(default)]
198    pub read_only: bool,
199    #[serde(default)]
200    pub write_only: bool,
201    /// JSON Schema `default` for the property. `ValueRef` indexes
202    /// into [`crate::Ir::values`]; compound defaults are pooled there.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub default: Option<ValueRef>,
205    /// `x-*` extensions declared on the property's schema. Each entry
206    /// pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
207    /// compound values are pooled there.
208    #[serde(default, skip_serializing_if = "Vec::is_empty")]
209    pub extensions: Vec<(String, ValueRef)>,
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213#[serde(tag = "kind", rename_all = "kebab-case")]
214pub enum AdditionalProperties {
215    Forbidden,
216    Any,
217    Typed { r#type: TypeRef },
218}
219
220#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
221pub struct ObjectConstraints {
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub min_properties: Option<u64>,
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub max_properties: Option<u64>,
226}
227
228// ---- enums ----
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
231pub struct EnumStringType {
232    pub values: Vec<EnumStringValue>,
233}
234
235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236pub struct EnumStringValue {
237    pub value: String,
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub documentation: Option<String>,
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243pub struct EnumIntType {
244    pub values: Vec<EnumIntValue>,
245    pub kind: IntKind,
246}
247
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub struct EnumIntValue {
250    pub value: i64,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub documentation: Option<String>,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
256#[serde(rename_all = "kebab-case")]
257pub enum IntKind {
258    Int32,
259    Int64,
260}
261
262// ---- unions ----
263
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
265pub struct UnionType {
266    pub variants: Vec<UnionVariant>,
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub discriminator: Option<Discriminator>,
269    pub kind: UnionKind,
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
273pub struct UnionVariant {
274    #[serde(rename = "type")]
275    pub r#type: TypeRef,
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub tag: Option<String>,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "kebab-case")]
282pub enum UnionKind {
283    OneOf,
284    AnyOf,
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
288pub struct Discriminator {
289    pub property_name: String,
290    pub mapping: Vec<(String, TypeRef)>,
291    /// `x-*` extensions declared on the discriminator object. Each
292    /// entry pairs a key with a [`ValueRef`] into [`crate::Ir::values`];
293    /// compound extension values are pooled there.
294    #[serde(default, skip_serializing_if = "Vec::is_empty")]
295    pub extensions: Vec<(String, ValueRef)>,
296}