Skip to content

Commit ed45479

Browse files
committed
Big messy commit. SQL, zod, new website, ...
1 parent b3ecdc7 commit ed45479

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+7620
-19
lines changed

.editorconfig

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ insert_final_newline = true
66
indent_style = space
77
indent_size = 4
88

9-
[*.{js,html,json,yml}]
9+
[*.{js,ts,html,json,yml}]
1010
indent_size = 2

json_typegen_shared/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ linked-hash-map = "0.5.4"
2828
syn = { version = "0.11", features = ["full", "parsing"], optional = true }
2929
synom = { version = "0.11.3", optional = true }
3030
indicatif = { version = "0.16.2", optional = true }
31+
sqlparser = "0.36.1"
3132

3233
[dev-dependencies]
3334
testsyn = { package = "syn", version = "0.15", features = ["full", "parsing", "extra-traits"] }

json_typegen_shared/benches/bench.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ macro_rules! file_bench {
2121
};
2222
}
2323

24-
file_bench!(magic_card_list, "fixtures/magic_card_list.json");
25-
file_bench!(zalando_article, "fixtures/zalando_article.json");
24+
file_bench!(magic_card_list, "fixtures/magicCardList.json");
25+
file_bench!(zalando_article, "fixtures/zalandoArticle.json");

json_typegen_shared/src/generation.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod rust;
55
pub mod shape;
66
pub mod typescript;
77
pub mod typescript_type_alias;
8+
pub mod zod_schema;
89

910
mod serde_case; // used in rust
1011
mod value; // used in json_schema and shape

json_typegen_shared/src/generation/typescript_type_alias.rs

+17-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use linked_hash_map::LinkedHashMap;
22

33
use crate::generation::typescript::{collapse_option, is_ts_identifier};
44
use crate::options::Options;
5-
use crate::shape::{self, Shape};
5+
use crate::shape::{self, common_shape, Shape};
66

77
pub struct Ctxt {
88
options: Options,
@@ -39,7 +39,17 @@ fn type_from_shape(ctxt: &mut Ctxt, shape: &Shape) -> Code {
3939
}
4040
}
4141
VecT { elem_type: e } => generate_vec_type(ctxt, e),
42-
Struct { fields } => generate_struct_from_field_shapes(ctxt, fields),
42+
Struct { fields } => {
43+
if ctxt.options.infer_map_threshold.is_some_and(|lim| { fields.len() > lim }) {
44+
let inner = fields
45+
.into_iter()
46+
.map(|(_, value)| value.clone())
47+
.fold(Shape::Bottom, common_shape);
48+
generate_map_type(ctxt, &inner)
49+
} else {
50+
generate_struct_from_field_shapes(ctxt, fields)
51+
}
52+
}
4353
MapT { val_type: v } => generate_map_type(ctxt, v),
4454
Opaque(t) => t.clone(),
4555
Optional(e) => {
@@ -49,15 +59,15 @@ fn type_from_shape(ctxt: &mut Ctxt, shape: &Shape) -> Code {
4959
} else {
5060
format!("{} | undefined", inner)
5161
}
52-
},
62+
}
5363
Nullable(e) => {
5464
let inner = type_from_shape(ctxt, e);
5565
if ctxt.options.use_default_for_missing_fields {
5666
inner
5767
} else {
5868
format!("{} | null", inner)
5969
}
60-
},
70+
}
6171
}
6272
}
6373

@@ -67,8 +77,9 @@ fn generate_vec_type(ctxt: &mut Ctxt, shape: &Shape) -> Code {
6777
}
6878

6979
fn generate_map_type(ctxt: &mut Ctxt, shape: &Shape) -> Code {
70-
let inner = type_from_shape(ctxt, shape);
71-
format!("{{ [key: string]: {} }}", inner)
80+
let (_was_optional, collapsed) = collapse_option(shape);
81+
let inner = type_from_shape(ctxt, collapsed);
82+
format!("Record<string, {}>", inner)
7283
}
7384

7485
fn generate_tuple_type(ctxt: &mut Ctxt, shapes: &[Shape]) -> Code {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use linked_hash_map::LinkedHashMap;
2+
3+
use crate::generation::typescript::{collapse_option, is_ts_identifier};
4+
use crate::options::Options;
5+
use crate::shape::{self, common_shape, Shape};
6+
use crate::util::lower_camel_case;
7+
8+
pub struct Ctxt {
9+
options: Options,
10+
indent_level: usize,
11+
}
12+
13+
pub type Code = String;
14+
15+
pub fn zod_schema(name: &str, shape: &Shape, options: Options) -> Code {
16+
let mut ctxt = Ctxt {
17+
options,
18+
indent_level: 1,
19+
};
20+
21+
let code = type_from_shape(&mut ctxt, shape);
22+
let mut schema_name = lower_camel_case(name);
23+
schema_name.push_str("Schema");
24+
25+
format!("export const {} = {};\n\n", schema_name, code)
26+
}
27+
28+
fn type_from_shape(ctxt: &mut Ctxt, shape: &Shape) -> Code {
29+
use crate::shape::Shape::*;
30+
match shape {
31+
Null | Any | Bottom => "z.unknown()".into(),
32+
Bool => "z.boolean()".into(),
33+
StringT => "z.string()".into(),
34+
Integer => "z.number()".into(),
35+
Floating => "z.number()".into(),
36+
Tuple(shapes, _n) => {
37+
let folded = shape::fold_shapes(shapes.clone());
38+
if folded == Any && shapes.iter().any(|s| s != &Any) {
39+
generate_tuple_type(ctxt, shapes)
40+
} else {
41+
generate_vec_type(ctxt, &folded)
42+
}
43+
}
44+
VecT { elem_type: e } => generate_vec_type(ctxt, e),
45+
Struct { fields } => {
46+
if ctxt.options.infer_map_threshold.is_some_and(|lim| { fields.len() > lim }) {
47+
let inner = fields
48+
.into_iter()
49+
.map(|(_, value)| value.clone())
50+
.fold(Shape::Bottom, common_shape);
51+
generate_map_type(ctxt, &inner)
52+
} else {
53+
generate_struct_from_field_shapes(ctxt, fields)
54+
}
55+
}
56+
MapT { val_type: v } => generate_map_type(ctxt, v),
57+
Opaque(t) => t.clone(),
58+
Optional(e) => {
59+
let inner = type_from_shape(ctxt, e);
60+
if ctxt.options.use_default_for_missing_fields {
61+
inner
62+
} else {
63+
format!("{}.optional()", inner)
64+
}
65+
},
66+
Nullable(e) => {
67+
let inner = type_from_shape(ctxt, e);
68+
if ctxt.options.use_default_for_missing_fields {
69+
inner
70+
} else {
71+
format!("{}.nullable()", inner)
72+
}
73+
},
74+
}
75+
}
76+
77+
fn generate_vec_type(ctxt: &mut Ctxt, shape: &Shape) -> Code {
78+
let inner = type_from_shape(ctxt, shape);
79+
format!("{}.array()", inner)
80+
}
81+
82+
fn generate_map_type(ctxt: &mut Ctxt, shape: &Shape) -> Code {
83+
let (_was_optional, collapsed) = collapse_option(shape);
84+
let inner = type_from_shape(ctxt, collapsed);
85+
format!("z.record(z.string(), {})", inner)
86+
}
87+
88+
fn generate_tuple_type(ctxt: &mut Ctxt, shapes: &[Shape]) -> Code {
89+
let mut types = Vec::new();
90+
91+
for shape in shapes {
92+
let typ = type_from_shape(ctxt, shape);
93+
types.push(typ);
94+
}
95+
96+
format!("z.tuple([{}])", types.join(", "))
97+
}
98+
99+
fn generate_struct_from_field_shapes(ctxt: &mut Ctxt, map: &LinkedHashMap<String, Shape>) -> Code {
100+
let fields: Vec<Code> = map
101+
.iter()
102+
.map(|(name, typ)| {
103+
ctxt.indent_level += 1;
104+
let field_type = type_from_shape(ctxt, typ);
105+
ctxt.indent_level -= 1;
106+
107+
let escape_name = !is_ts_identifier(name);
108+
109+
format!(
110+
"{}{}{}{}: {};",
111+
" ".repeat(ctxt.indent_level),
112+
if escape_name { "\"" } else { "" },
113+
name,
114+
if escape_name { "\"" } else { "" },
115+
field_type
116+
)
117+
})
118+
.collect();
119+
120+
let mut code = "z.object({\n".to_string();
121+
122+
if !fields.is_empty() {
123+
code += &fields.join("\n");
124+
code += "\n";
125+
}
126+
code += &" ".repeat(ctxt.indent_level - 1);
127+
code += "})";
128+
129+
code
130+
}

json_typegen_shared/src/hints.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl<'a> Hints<'a> {
7272

7373
/// ([/a/b, /a/c, /d/e], "a") -> [/b, /c]
7474
pub fn step_field(&self, name: &str) -> Hints {
75-
self.step(|first| first == name)
75+
self.step(|first| first == "-" || first == name)
7676
}
7777

7878
/// [/1/b, /a/c, /-/e] -> [/b, /c, /e]

json_typegen_shared/src/inference/jsoninfer.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,23 @@ impl<T: Iterator<Item = Result<JsonToken, JsonInputErr>>> Inference<T> {
133133

134134
if let Some(&Ok(JsonToken::ObjectEnd)) = self.tokens.peek() {
135135
self.tokens.next();
136-
return Ok(Shape::Struct { fields });
136+
break;
137137
}
138138

139139
self.expect_token(JsonToken::Comma)?;
140140
}
141+
142+
if options.infer_map_threshold.is_some_and(|lim| { fields.len() > lim }) {
143+
let inner = fields
144+
.into_iter()
145+
.map(|(_, value)| value)
146+
.fold(Shape::Bottom, common_shape);
147+
Ok(Shape::MapT {
148+
val_type: Box::new(inner),
149+
})
150+
} else {
151+
Ok(Shape::Struct { fields })
152+
}
141153
}
142154

143155
fn infer_array(&mut self, options: &Options, hints: &Hints) -> Result<Shape, JsonInputErr> {

json_typegen_shared/src/lib.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mod progress;
2323
mod shape;
2424
mod to_singular;
2525
mod util;
26+
mod sql;
2627

2728
use crate::hints::Hints;
2829
use crate::inference::shape_from_json;
@@ -44,6 +45,8 @@ pub enum JTError {
4445
SampleReadingError(#[from] std::io::Error),
4546
#[error("An error occurred while parsing JSON")]
4647
JsonParsingError(#[from] inference::JsonInputErr),
48+
#[error("An error occurred while parsing SQL: {0}")]
49+
SqlParsingError(String),
4750
#[error("An error occurred while parsing a macro or macro input: {0}")]
4851
MacroParsingError(String),
4952
}
@@ -106,7 +109,16 @@ pub fn codegen(name: &str, input: &str, mut options: Options) -> Result<String,
106109
hints.add(pointer, hint);
107110
}
108111

109-
let shape = infer_from_sample(&source, &options, &hints)?;
112+
let shape = match options.input_mode {
113+
options::InputMode::Sql => {
114+
let shapes = sql::sql_to_shape(input).map_err(JTError::SqlParsingError)?;
115+
let (_name, shap) = shapes.get(0).unwrap();
116+
shap.clone()
117+
}
118+
options::InputMode::Json => {
119+
infer_from_sample(&source, &options, &hints)?
120+
}
121+
};
110122

111123
codegen_from_shape(name, &shape, options)
112124
}
@@ -116,6 +128,7 @@ pub fn codegen_from_shape(name: &str, shape: &Shape, options: Options) -> Result
116128
let mut generated_code = match options.output_mode {
117129
OutputMode::Rust => generation::rust::rust_types(name, shape, options),
118130
OutputMode::JsonSchema => generation::json_schema::json_schema(name, shape, options),
131+
OutputMode::ZodSchema => generation::zod_schema::zod_schema(name, shape, options),
119132
OutputMode::KotlinJackson | OutputMode::KotlinKotlinx => {
120133
generation::kotlin::kotlin_types(name, shape, options)
121134
}

json_typegen_shared/src/options.rs

+23
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::hints::Hint;
77
#[derive(Debug, PartialEq, Clone)]
88
pub struct Options {
99
pub output_mode: OutputMode,
10+
pub input_mode: InputMode,
1011
pub use_default_for_missing_fields: bool,
1112
pub deny_unknown_fields: bool,
1213
pub(crate) allow_option_vec: bool,
@@ -18,12 +19,14 @@ pub struct Options {
1819
pub unwrap: String,
1920
pub import_style: ImportStyle,
2021
pub collect_additional: bool,
22+
pub infer_map_threshold: Option<usize>,
2123
}
2224

2325
impl Default for Options {
2426
fn default() -> Options {
2527
Options {
2628
output_mode: OutputMode::Rust,
29+
input_mode: InputMode::Json,
2730
use_default_for_missing_fields: false,
2831
deny_unknown_fields: false,
2932
allow_option_vec: false,
@@ -35,6 +38,7 @@ impl Default for Options {
3538
unwrap: "".into(),
3639
import_style: ImportStyle::AddImports,
3740
collect_additional: false,
41+
infer_map_threshold: None,
3842
}
3943
}
4044
}
@@ -82,6 +86,7 @@ pub enum OutputMode {
8286
KotlinKotlinx,
8387
PythonPydantic,
8488
JsonSchema,
89+
ZodSchema,
8590
Shape,
8691
}
8792

@@ -96,12 +101,30 @@ impl OutputMode {
96101
"kotlin/kotlinx" => Some(OutputMode::KotlinKotlinx),
97102
"python" => Some(OutputMode::PythonPydantic),
98103
"json_schema" => Some(OutputMode::JsonSchema),
104+
"zod" => Some(OutputMode::ZodSchema),
99105
"shape" => Some(OutputMode::Shape),
100106
_ => None,
101107
}
102108
}
103109
}
104110

111+
#[non_exhaustive]
112+
#[derive(Debug, PartialEq, Clone)]
113+
pub enum InputMode {
114+
Json,
115+
Sql,
116+
}
117+
118+
impl InputMode {
119+
pub fn parse(s: &str) -> Option<Self> {
120+
match s {
121+
"json" => Some(InputMode::Json),
122+
"sql" => Some(InputMode::Sql),
123+
_ => None,
124+
}
125+
}
126+
}
127+
105128
// https://serde.rs/container-attrs.html rename_all:
106129
// "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case",
107130
// "SCREAMING_SNAKE_CASE", "kebab-case", "SCREAMING-KEBAB-CASE"

json_typegen_shared/src/parse.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use syn::parse::{boolean, ident, string};
77
use synom::{alt, call, named, punct, IResult};
88

99
use crate::hints::Hint;
10-
use crate::options::{ImportStyle, Options, OutputMode, StringTransform};
10+
use crate::options::{ImportStyle, InputMode, Options, OutputMode, StringTransform};
1111

1212
#[derive(PartialEq, Debug)]
1313
pub struct MacroInput {
@@ -114,6 +114,9 @@ fn options_with_defaults(input: &str, default_options: Options) -> Result<Option
114114
"output_mode" => string_option(remaining, "output_mode", |val| {
115115
options.output_mode = OutputMode::parse(&val).unwrap_or(OutputMode::Rust);
116116
}),
117+
"input_mode" => string_option(remaining, "input_mode", |val| {
118+
options.input_mode = InputMode::parse(&val).unwrap_or(InputMode::Json);
119+
}),
117120
"derives" => string_option(remaining, "derives", |val| {
118121
options.derives = val;
119122
}),
@@ -143,6 +146,9 @@ fn options_with_defaults(input: &str, default_options: Options) -> Result<Option
143146
"unwrap" => string_option(remaining, "unwrap", |val| {
144147
options.unwrap = val;
145148
}),
149+
"infer_map_threshold" => string_option(remaining, "infer_map_threshold", |val| {
150+
options.infer_map_threshold = val.parse().ok();
151+
}),
146152
key if key.is_empty() || key.starts_with('/') => {
147153
let (rem, hints) = pointer_block(remaining)?;
148154
for hint in hints {

0 commit comments

Comments
 (0)