1
1
use std:: { borrow:: Cow , collections:: BTreeMap , sync:: Arc } ;
2
2
3
- use aide:: openapi:: { self } ;
3
+ use aide:: openapi;
4
4
use anyhow:: { bail, ensure, Context as _} ;
5
5
use schemars:: schema:: { InstanceType , Schema , SchemaObject , SingleOrVec } ;
6
+ use serde:: Serialize ;
6
7
7
8
use crate :: util:: get_schema_name;
8
9
9
10
/// Named types referenced by the [`Api`].
10
11
///
11
12
/// Intermediate representation of (some) `components` from the spec.
12
- #[ allow( dead_code) ] // FIXME: Remove when we generate "model" files
13
13
#[ derive( Debug ) ]
14
- pub ( crate ) struct Types ( pub BTreeMap < String , SchemaObject > ) ;
14
+ pub ( crate ) struct Types ( pub BTreeMap < String , Type > ) ;
15
+
16
+ #[ derive( Debug , Serialize ) ]
17
+ pub ( crate ) struct Type {
18
+ name : String ,
19
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
20
+ description : Option < String > ,
21
+ deprecated : bool ,
22
+ #[ serde( flatten) ]
23
+ data : TypeData ,
24
+ }
25
+
26
+ impl Type {
27
+ pub ( crate ) fn from_schema ( name : String , s : SchemaObject ) -> anyhow:: Result < Self > {
28
+ match s. instance_type {
29
+ Some ( SingleOrVec :: Single ( it) ) => match * it {
30
+ InstanceType :: Object => { }
31
+ _ => bail ! ( "unsupported type {it:?}" ) ,
32
+ } ,
33
+ Some ( SingleOrVec :: Vec ( _) ) => bail ! ( "unsupported: multiple types" ) ,
34
+ None => bail ! ( "unsupported: no type" ) ,
35
+ }
36
+
37
+ let metadata = s. metadata . unwrap_or_default ( ) ;
38
+
39
+ let obj = s
40
+ . object
41
+ . context ( "unsupported: object type without further validation" ) ?;
42
+
43
+ ensure ! (
44
+ obj. additional_properties. is_none( ) ,
45
+ "additional_properties not yet supported"
46
+ ) ;
47
+ ensure ! ( obj. max_properties. is_none( ) , "unsupported: max_properties" ) ;
48
+ ensure ! ( obj. min_properties. is_none( ) , "unsupported: min_properties" ) ;
49
+ ensure ! (
50
+ obj. pattern_properties. is_empty( ) ,
51
+ "unsupported: pattern_properties"
52
+ ) ;
53
+ ensure ! ( obj. property_names. is_none( ) , "unsupported: property_names" ) ;
54
+
55
+ Ok ( Self {
56
+ name,
57
+ description : metadata. description ,
58
+ deprecated : metadata. deprecated ,
59
+ data : TypeData :: Struct {
60
+ fields : obj
61
+ . properties
62
+ . into_iter ( )
63
+ . map ( |( name, schema) | {
64
+ Field :: from_schema ( name. clone ( ) , schema, obj. required . contains ( & name) )
65
+ . with_context ( || format ! ( "unsupported field {name}" ) )
66
+ } )
67
+ . collect :: < anyhow:: Result < _ > > ( ) ?,
68
+ } ,
69
+ } )
70
+ }
71
+ }
72
+
73
+ #[ derive( Debug , Serialize ) ]
74
+ #[ serde( untagged) ]
75
+ pub ( crate ) enum TypeData {
76
+ Struct {
77
+ fields : Vec < Field > ,
78
+ } ,
79
+ #[ allow( dead_code) ] // not _yet_ supported
80
+ Enum {
81
+ variants : Vec < Variant > ,
82
+ } ,
83
+ }
84
+
85
+ #[ derive( Debug , Serialize ) ]
86
+ pub ( crate ) struct Field {
87
+ name : String ,
88
+ r#type : FieldType ,
89
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
90
+ default : Option < serde_json:: Value > ,
91
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
92
+ description : Option < String > ,
93
+ required : bool ,
94
+ nullable : bool ,
95
+ deprecated : bool ,
96
+ }
97
+
98
+ impl Field {
99
+ fn from_schema ( name : String , s : Schema , required : bool ) -> anyhow:: Result < Self > {
100
+ let obj = match s {
101
+ Schema :: Bool ( _) => bail ! ( "unsupported bool schema" ) ,
102
+ Schema :: Object ( o) => o,
103
+ } ;
104
+ let metadata = obj. metadata . clone ( ) . unwrap_or_default ( ) ;
105
+
106
+ ensure ! ( obj. const_value. is_none( ) , "unsupported const_value" ) ;
107
+ ensure ! ( obj. enum_values. is_none( ) , "unsupported enum_values" ) ;
108
+
109
+ let nullable = obj
110
+ . extensions
111
+ . get ( "nullable" )
112
+ . and_then ( |v| v. as_bool ( ) )
113
+ . unwrap_or ( false ) ;
114
+
115
+ Ok ( Self {
116
+ name,
117
+ r#type : FieldType :: from_schema_object ( obj) ?,
118
+ default : metadata. default ,
119
+ description : metadata. description ,
120
+ required,
121
+ nullable,
122
+ deprecated : metadata. deprecated ,
123
+ } )
124
+ }
125
+ }
126
+
127
+ #[ derive( Debug , Serialize ) ]
128
+ pub ( crate ) struct Variant {
129
+ fields : Vec < Field > ,
130
+ }
15
131
16
132
/// Supported field type.
17
133
///
18
134
/// Equivalent to openapi's `type` + `format` + `$ref`.
19
135
#[ derive( Clone , Debug ) ]
20
136
pub ( crate ) enum FieldType {
21
137
Bool ,
138
+ Int16 ,
139
+ UInt16 ,
140
+ Int32 ,
141
+ Int64 ,
22
142
UInt64 ,
23
143
String ,
24
144
DateTime ,
145
+ Uri ,
146
+ /// A JSON object with arbitrary field values.
147
+ JsonObject ,
148
+ /// A regular old list.
149
+ List ( Box < FieldType > ) ,
25
150
/// List with unique items.
26
151
Set ( Box < FieldType > ) ,
152
+ /// A map with a given value type.
153
+ ///
154
+ /// The key type is always `String` in JSON schemas.
155
+ Map {
156
+ value_ty : Box < FieldType > ,
157
+ } ,
27
158
SchemaRef ( String ) ,
28
159
}
29
160
@@ -32,40 +163,85 @@ impl FieldType {
32
163
let openapi:: ParameterSchemaOrContent :: Schema ( s) = format else {
33
164
bail ! ( "found unexpected 'content' data format" ) ;
34
165
} ;
35
- Self :: from_json_schema ( s. json_schema )
166
+ Self :: from_schema ( s. json_schema )
36
167
}
37
168
38
- fn from_json_schema ( s : Schema ) -> anyhow:: Result < Self > {
169
+ fn from_schema ( s : Schema ) -> anyhow:: Result < Self > {
39
170
let Schema :: Object ( obj) = s else {
40
171
bail ! ( "found unexpected `true` schema" ) ;
41
172
} ;
42
173
43
- Ok ( match obj. instance_type {
44
- Some ( SingleOrVec :: Single ( ty) ) => match * ty {
174
+ Self :: from_schema_object ( obj)
175
+ }
176
+
177
+ fn from_schema_object ( obj : SchemaObject ) -> anyhow:: Result < FieldType > {
178
+ Ok ( match & obj. instance_type {
179
+ Some ( SingleOrVec :: Single ( ty) ) => match * * ty {
45
180
InstanceType :: Boolean => Self :: Bool ,
46
181
InstanceType :: Integer => match obj. format . as_deref ( ) {
47
- Some ( "uint64" ) => Self :: UInt64 ,
182
+ Some ( "int16" ) => Self :: Int16 ,
183
+ Some ( "uint16" ) => Self :: UInt16 ,
184
+ Some ( "int32" ) => Self :: Int32 ,
185
+ // FIXME: Why do we have int in the spec?
186
+ Some ( "int" | "int64" ) => Self :: Int64 ,
187
+ // FIXME: Get rid of uint in the spec..
188
+ Some ( "uint" | "uint64" ) => Self :: UInt64 ,
48
189
f => bail ! ( "unsupported integer format: `{f:?}`" ) ,
49
190
} ,
50
191
InstanceType :: String => match obj. format . as_deref ( ) {
51
192
None => Self :: String ,
52
193
Some ( "date-time" ) => Self :: DateTime ,
194
+ Some ( "uri" ) => Self :: Uri ,
53
195
Some ( f) => bail ! ( "unsupported string format: `{f:?}`" ) ,
54
196
} ,
55
197
InstanceType :: Array => {
56
198
let array = obj. array . context ( "array type must have array props" ) ?;
57
199
ensure ! ( array. additional_items. is_none( ) , "not supported" ) ;
58
- ensure ! (
59
- array. unique_items == Some ( true ) ,
60
- "non-setlike arrays not currently supported"
61
- ) ;
62
200
let inner = match array. items . context ( "array type must have items prop" ) ? {
63
201
SingleOrVec :: Single ( ty) => ty,
64
202
SingleOrVec :: Vec ( types) => {
65
203
bail ! ( "unsupported multi-typed array parameter: `{types:?}`" )
66
204
}
67
205
} ;
68
- Self :: Set ( Box :: new ( Self :: from_json_schema ( * inner) ?) )
206
+ let inner = Box :: new ( Self :: from_schema ( * inner) ?) ;
207
+ if array. unique_items == Some ( true ) {
208
+ Self :: Set ( inner)
209
+ } else {
210
+ Self :: List ( inner)
211
+ }
212
+ }
213
+ InstanceType :: Object => {
214
+ let obj = obj
215
+ . object
216
+ . context ( "unsupported: object type without further validation" ) ?;
217
+ let additional_properties = obj
218
+ . additional_properties
219
+ . context ( "unsupported: object field type without additional_properties" ) ?;
220
+
221
+ ensure ! ( obj. max_properties. is_none( ) , "unsupported: max_properties" ) ;
222
+ ensure ! ( obj. min_properties. is_none( ) , "unsupported: min_properties" ) ;
223
+ ensure ! (
224
+ obj. properties. is_empty( ) ,
225
+ "unsupported: properties on field type"
226
+ ) ;
227
+ ensure ! (
228
+ obj. pattern_properties. is_empty( ) ,
229
+ "unsupported: pattern_properties"
230
+ ) ;
231
+ ensure ! ( obj. property_names. is_none( ) , "unsupported: property_names" ) ;
232
+ ensure ! (
233
+ obj. required. is_empty( ) ,
234
+ "unsupported: required on field type"
235
+ ) ;
236
+
237
+ match * additional_properties {
238
+ Schema :: Bool ( true ) => Self :: JsonObject ,
239
+ Schema :: Bool ( false ) => bail ! ( "unsupported `additional_properties: false`" ) ,
240
+ Schema :: Object ( schema_object) => {
241
+ let value_ty = Box :: new ( Self :: from_schema_object ( schema_object) ?) ;
242
+ Self :: Map { value_ty }
243
+ }
244
+ }
69
245
}
70
246
ty => bail ! ( "unsupported type: `{ty:?}`" ) ,
71
247
} ,
@@ -82,60 +258,91 @@ impl FieldType {
82
258
fn to_csharp_typename ( & self ) -> Cow < ' _ , str > {
83
259
match self {
84
260
Self :: Bool => "bool" . into ( ) ,
261
+ Self :: Int32 |
85
262
// FIXME: For backwards compatibility. Should be 'long'.
86
- Self :: UInt64 => "int" . into ( ) ,
263
+ Self :: Int64 | Self :: UInt64 => "int" . into ( ) ,
87
264
Self :: String => "string" . into ( ) ,
88
265
Self :: DateTime => "DateTime" . into ( ) ,
89
- Self :: Set ( field_type) => format ! ( "List<{}>" , field_type. to_csharp_typename( ) ) . into ( ) ,
266
+ Self :: Int16 | Self :: UInt16 | Self :: Uri | Self :: JsonObject | Self :: Map { .. } => todo ! ( ) ,
267
+ // FIXME: Treat set differently?
268
+ Self :: List ( field_type) | Self :: Set ( field_type) => {
269
+ format ! ( "List<{}>" , field_type. to_csharp_typename( ) ) . into ( )
270
+ }
90
271
Self :: SchemaRef ( name) => name. clone ( ) . into ( ) ,
91
272
}
92
273
}
93
274
94
275
fn to_go_typename ( & self ) -> Cow < ' _ , str > {
95
276
match self {
96
277
Self :: Bool => "bool" . into ( ) ,
278
+ Self :: Int32 |
97
279
// FIXME: Looks like all integers are currently i32
98
- Self :: UInt64 => "int32" . into ( ) ,
280
+ Self :: Int64 | Self :: UInt64 => "int32" . into ( ) ,
99
281
Self :: String => "string" . into ( ) ,
100
282
Self :: DateTime => "time.Time" . into ( ) ,
101
- Self :: Set ( field_type) => format ! ( "[]{}" , field_type. to_go_typename( ) ) . into ( ) ,
283
+ Self :: Int16 | Self :: UInt16 | Self :: Uri | Self :: JsonObject | Self :: Map { .. } => todo ! ( ) ,
284
+ Self :: List ( field_type) | Self :: Set ( field_type) => {
285
+ format ! ( "[]{}" , field_type. to_go_typename( ) ) . into ( )
286
+ }
102
287
Self :: SchemaRef ( name) => name. clone ( ) . into ( ) ,
103
288
}
104
289
}
105
290
106
291
fn to_kotlin_typename ( & self ) -> Cow < ' _ , str > {
107
292
match self {
108
293
Self :: Bool => "Boolean" . into ( ) ,
294
+ Self :: Int32 |
109
295
// FIXME: Should be Long..
110
- Self :: UInt64 => "Int" . into ( ) ,
296
+ Self :: Int64 | Self :: UInt64 => "Int" . into ( ) ,
111
297
Self :: String => "String" . into ( ) ,
112
298
Self :: DateTime => "OffsetDateTime" . into ( ) ,
113
- Self :: Set ( field_type) => format ! ( "List<{}>" , field_type. to_kotlin_typename( ) ) . into ( ) ,
299
+ Self :: Int16 | Self :: UInt16 | Self :: Uri | Self :: JsonObject | Self :: Map { .. } => todo ! ( ) ,
300
+ // FIXME: Treat set differently?
301
+ Self :: List ( field_type) | Self :: Set ( field_type) => {
302
+ format ! ( "List<{}>" , field_type. to_kotlin_typename( ) ) . into ( )
303
+ }
114
304
Self :: SchemaRef ( name) => name. clone ( ) . into ( ) ,
115
305
}
116
306
}
117
307
118
308
fn to_js_typename ( & self ) -> Cow < ' _ , str > {
119
309
match self {
120
310
Self :: Bool => "boolean" . into ( ) ,
121
- Self :: UInt64 => "number" . into ( ) ,
311
+ Self :: Int16 | Self :: UInt16 | Self :: Int32 | Self :: Int64 | Self :: UInt64 => {
312
+ "number" . into ( )
313
+ }
122
314
Self :: String => "string" . into ( ) ,
123
315
Self :: DateTime => "Date | null" . into ( ) ,
124
- Self :: Set ( field_type) => format ! ( "{}[]" , field_type. to_js_typename( ) ) . into ( ) ,
316
+ Self :: Uri | Self :: JsonObject | Self :: Map { .. } => todo ! ( ) ,
317
+ Self :: List ( field_type) | Self :: Set ( field_type) => {
318
+ format ! ( "{}[]" , field_type. to_js_typename( ) ) . into ( )
319
+ }
125
320
Self :: SchemaRef ( name) => name. clone ( ) . into ( ) ,
126
321
}
127
322
}
128
323
129
324
fn to_rust_typename ( & self ) -> Cow < ' _ , str > {
130
325
match self {
131
326
Self :: Bool => "bool" . into ( ) ,
132
- // FIXME: Looks like all integers are currently i32
133
- Self :: UInt64 => "i32" . into ( ) ,
134
- Self :: String => "String" . into ( ) ,
327
+ Self :: Int16 => "i16" . into ( ) ,
328
+ Self :: UInt16 => "u16" . into ( ) ,
329
+ Self :: Int32 |
330
+ // FIXME: All integers in query params are currently i32
331
+ Self :: Int64 | Self :: UInt64 => "i32" . into ( ) ,
332
+ // FIXME: Do we want a separate type for Uri?
333
+ Self :: Uri | Self :: String => "String" . into ( ) ,
135
334
// FIXME: Depends on those chrono imports being in scope, not that great..
136
335
Self :: DateTime => "DateTime<Utc>" . into ( ) ,
137
- // FIXME: Use BTreeSet
138
- Self :: Set ( field_type) => format ! ( "Vec<{}>" , field_type. to_rust_typename( ) ) . into ( ) ,
336
+ Self :: JsonObject => "serde_json::Value" . into ( ) ,
337
+ // FIXME: Treat set differently? (BTreeSet)
338
+ Self :: List ( field_type) | Self :: Set ( field_type) => {
339
+ format ! ( "Vec<{}>" , field_type. to_rust_typename( ) ) . into ( )
340
+ }
341
+ Self :: Map { value_ty } => format ! (
342
+ "std::collections::HashMap<String, {}>" ,
343
+ value_ty. to_rust_typename( ) ,
344
+ )
345
+ . into ( ) ,
139
346
Self :: SchemaRef ( name) => name. clone ( ) . into ( ) ,
140
347
}
141
348
}
0 commit comments