hydrate_codegen/
lib.rs

1use hydrate_data::{
2    Schema, SchemaEnum, SchemaNamedType, SchemaRecord, SchemaSet, SchemaSetBuilder,
3};
4use hydrate_pipeline::HydrateProjectConfiguration;
5use std::error::Error;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use structopt::StructOpt;
9
10//
11// TODO: Validation code - we should have a fn on generated types to verify they are registered in the schema and match
12// TODO: Optionally also generate code to register them as new schema types
13// TODO: Could cache a ref to a linked schema
14//
15
16#[derive(StructOpt, Debug, Default)]
17pub struct HydrateCodegenArgs {
18    // If no options are provided, we will run all jobs in hydrate_project.json
19
20    // If a job name is specified, we will look for the job in a hydrate_project.json
21    #[structopt(name = "job-name", long)]
22    pub job_name: Option<String>,
23
24    // If schema_path and outfile are used, we will consume that input file and write to the output file
25    #[structopt(name = "schema-path", long, parse(from_os_str))]
26    pub schema_path: Option<PathBuf>,
27    #[structopt(name = "outfile", long, parse(from_os_str))]
28    pub outfile: Option<PathBuf>,
29    #[structopt(name = "included-schema", long, parse(from_os_str))]
30    pub included_schema: Vec<PathBuf>,
31
32    #[structopt(name = "trace", long)]
33    pub trace: bool,
34}
35
36pub fn run(
37    project_file_serach_location: &Path,
38    args: &HydrateCodegenArgs,
39) -> Result<(), Box<dyn Error>> {
40    if args.schema_path.is_some() && args.outfile.is_some() {
41        return schema_to_rs(
42            args.schema_path.as_ref().unwrap(),
43            &args.included_schema,
44            args.outfile.as_ref().unwrap(),
45        );
46    }
47
48    if args.schema_path.is_some() != args.outfile.is_some() {
49        Err("--schema-path and --outfile both must be provided if either is provided")?;
50    }
51
52    // find the hydrate project file
53    let project_configuration =
54        HydrateProjectConfiguration::locate_project_file(project_file_serach_location).unwrap();
55
56    // If a job was specified, just run that job or error if it wasn't found
57    if let Some(job_name) = &args.job_name {
58        for schema_codegen_job in &project_configuration.schema_codegen_jobs {
59            if schema_codegen_job.name == *job_name {
60                log::info!("Run schema codegen job {}", &schema_codegen_job.name);
61                return schema_to_rs(
62                    &schema_codegen_job.schema_path,
63                    &schema_codegen_job.included_schema_paths,
64                    &schema_codegen_job.outfile,
65                );
66            }
67        }
68
69        Err("Could not find codegen job {} in hydrate_project.json")?;
70    }
71
72    // If nothing was specified run all schema codegen jobs
73    for schema_codegen_job in &project_configuration.schema_codegen_jobs {
74        log::info!("Run schema codegen job {}", &schema_codegen_job.name);
75        schema_to_rs(
76            &schema_codegen_job.schema_path,
77            &schema_codegen_job.included_schema_paths,
78            &schema_codegen_job.outfile,
79        )?
80    }
81
82    Ok(())
83}
84
85fn schema_to_rs(
86    schema_path: &Path,
87    referenced_schema_paths: &[PathBuf],
88    outfile: &Path,
89) -> Result<(), Box<dyn Error>> {
90    let mut linker = hydrate_data::SchemaLinker::default();
91    linker
92        .add_source_dir(&schema_path, "**.json")
93        .map_err(|x| Box::new(x))?;
94
95    let named_types_to_build = linker.unlinked_type_names();
96
97    for referenced_schema_path in referenced_schema_paths {
98        linker
99            .add_source_dir(referenced_schema_path, "**.json")
100            .map_err(|x| Box::new(x))?;
101    }
102
103    let mut schema_set_builder = SchemaSetBuilder::default();
104    schema_set_builder
105        .add_linked_types(linker)
106        .map_err(|x| Box::new(x))?;
107    let schema_set = schema_set_builder.build();
108
109    let mut all_schemas_to_build = Vec::default();
110    for named_type_to_build in named_types_to_build {
111        let named_type = schema_set
112            .find_named_type(named_type_to_build)
113            .expect("Cannot find linked type in built schema");
114        all_schemas_to_build.push((named_type.fingerprint(), named_type));
115    }
116
117    // Sort by name so we have a deterministic output ordering for codegen
118    all_schemas_to_build.sort_by(|lhs, rhs| lhs.1.name().cmp(rhs.1.name()));
119
120    let mut code_fragments_as_string = Vec::default();
121
122    for (_fingerprint, named_type) in all_schemas_to_build {
123        //println!("{:?} {:?}", fingerprint, named_type);
124
125        let scopes = match named_type {
126            SchemaNamedType::Record(x) => vec![
127                generate_accessor(&schema_set, x),
128                generate_reader(&schema_set, x),
129                generate_writer(&schema_set, x),
130                generate_owned(&schema_set, x),
131            ],
132            SchemaNamedType::Enum(x) => vec![generate_enum(&schema_set, x)],
133        };
134
135        for scope in scopes {
136            let code_fragment_as_string = scope.to_string();
137            //println!("{}\n", code_fragment_as_string);
138            code_fragments_as_string.push(code_fragment_as_string);
139        }
140    }
141
142    //let write_path = PathBuf::from("out_codegen.rs");
143    let f = std::fs::File::create(outfile)?;
144    let mut writer = std::io::BufWriter::new(f);
145    writeln!(writer, "// This file generated automatically by hydrate-codegen. Do not make manual edits. Use include!() to place these types in the intended location.")?;
146    for code_fragment in code_fragments_as_string {
147        writeln!(writer, "{}", &code_fragment)?;
148    }
149
150    writer.flush()?;
151    Ok(())
152}
153
154fn generate_enum(
155    _schema_set: &SchemaSet,
156    schema: &SchemaEnum,
157) -> codegen::Scope {
158    let mut scope = codegen::Scope::new();
159
160    let enum_name = format!("{}Enum", schema.name());
161    let enumeration = scope.new_enum(&enum_name);
162    enumeration.vis("pub");
163    enumeration.derive("Copy");
164    enumeration.derive("Clone");
165    for symbol in schema.symbols() {
166        enumeration.push_variant(codegen::Variant::new(symbol.name()));
167    }
168
169    let enum_impl = scope.new_impl(&enum_name).impl_trait("Enum");
170
171    let to_symbol_name_fn = enum_impl.new_fn("to_symbol_name");
172    to_symbol_name_fn.arg_ref_self().ret("&'static str");
173    to_symbol_name_fn.line("match self {");
174    for symbol in schema.symbols() {
175        to_symbol_name_fn.line(format!(
176            "    {}::{} => \"{}\",",
177            enum_name,
178            symbol.name(),
179            symbol.name()
180        ));
181    }
182    to_symbol_name_fn.line("}");
183
184    let from_symbol_name_fn = enum_impl.new_fn("from_symbol_name");
185    from_symbol_name_fn
186        .arg("str", "&str")
187        .ret(format!("Option<{}>", &enum_name));
188    from_symbol_name_fn.line("match str {");
189    for symbol in schema.symbols() {
190        from_symbol_name_fn.line(format!(
191            "    \"{}\" => Some({}::{}),",
192            symbol.name(),
193            enum_name,
194            symbol.name()
195        ));
196        for alias in symbol.aliases() {
197            from_symbol_name_fn.line(format!(
198                "    \"{}\" => Some({}::{}),",
199                alias,
200                enum_name,
201                symbol.name()
202            ));
203        }
204    }
205    from_symbol_name_fn.line("    _ => None,");
206    from_symbol_name_fn.line("}");
207
208    let main_impl = scope.new_impl(enum_name.as_str());
209    let schema_name_fn = main_impl.new_fn("schema_name");
210    schema_name_fn.ret("&'static str");
211    schema_name_fn.vis("pub");
212    schema_name_fn.line(format!("\"{}\"", schema.name()));
213
214    scope
215}
216
217fn field_schema_to_field_type(
218    schema_set: &SchemaSet,
219    field_schema: &Schema,
220) -> Option<String> {
221    Some(match field_schema {
222        Schema::Nullable(x) => format!(
223            "NullableFieldAccessor::<{}>",
224            field_schema_to_field_type(schema_set, &*x)?
225        ),
226        Schema::Boolean => "BooleanFieldAccessor".to_string(),
227        Schema::I32 => "I32FieldAccessor".to_string(),
228        Schema::I64 => "I64FieldAccessor".to_string(),
229        Schema::U32 => "U32FieldAccessor".to_string(),
230        Schema::U64 => "U64FieldAccessor".to_string(),
231        Schema::F32 => "F32FieldAccessor".to_string(),
232        Schema::F64 => "F64FieldAccessor".to_string(),
233        Schema::Bytes => "BytesFieldAccessor".to_string(),
234        Schema::String => "StringFieldAccessor".to_string(),
235        Schema::StaticArray(x) => format!(
236            "StaticArrayFieldAccessor::<{}>",
237            field_schema_to_field_type(schema_set, x.item_type())?
238        ),
239        Schema::DynamicArray(x) => format!(
240            "DynamicArrayFieldAccessor::<{}>",
241            field_schema_to_field_type(schema_set, x.item_type())?
242        ),
243        Schema::Map(x) => format!(
244            "MapFieldAccessor::<{}, {}>",
245            field_schema_to_field_type(schema_set, x.key_type())?,
246            field_schema_to_field_type(schema_set, x.value_type())?
247        ),
248        Schema::AssetRef(_x) => "AssetRefFieldAccessor".to_string(),
249        Schema::Record(x) | Schema::Enum(x) => {
250            let inner_type = schema_set.find_named_type_by_fingerprint(*x).unwrap();
251
252            match inner_type {
253                SchemaNamedType::Record(_) => format!("{}Accessor", inner_type.name().to_string()),
254                SchemaNamedType::Enum(_) => {
255                    format!("EnumFieldAccessor::<{}Enum>", inner_type.name().to_string())
256                }
257            }
258        }
259    })
260}
261
262fn generate_accessor(
263    schema_set: &SchemaSet,
264    schema: &SchemaRecord,
265) -> codegen::Scope {
266    let mut scope = codegen::Scope::new();
267
268    let accessor_name = format!("{}Accessor", schema.name());
269    let s = scope
270        .new_struct(accessor_name.as_str())
271        .tuple_field("PropertyPath");
272    s.vis("pub");
273    s.derive("Default");
274
275    let field_impl = scope
276        .new_impl(accessor_name.as_str())
277        .impl_trait("FieldAccessor");
278    let new_fn = field_impl
279        .new_fn("new")
280        .arg("property_path", "PropertyPath");
281    new_fn.ret("Self");
282    new_fn.line(format!("{}(property_path)", accessor_name));
283
284    let accessor_impl = scope
285        .new_impl(accessor_name.as_str())
286        .impl_trait("RecordAccessor");
287    let schema_name_fn = accessor_impl.new_fn("schema_name");
288    schema_name_fn.ret("&'static str");
289    schema_name_fn.line(format!("\"{}\"", schema.name()));
290
291    let main_impl = scope.new_impl(accessor_name.as_str());
292    for field in schema.fields() {
293        let field_type = field_schema_to_field_type(schema_set, field.field_schema());
294        if let Some(field_type) = field_type {
295            let field_access_fn = main_impl.new_fn(field.name());
296            field_access_fn.arg_ref_self();
297            field_access_fn.ret(&field_type);
298            field_access_fn.vis("pub");
299            field_access_fn.line(format!(
300                "{}::new(self.0.push(\"{}\"))",
301                field_type,
302                field.name()
303            ));
304        }
305    }
306
307    scope
308}
309
310fn field_schema_to_reader_type(
311    schema_set: &SchemaSet,
312    field_schema: &Schema,
313) -> Option<String> {
314    Some(match field_schema {
315        Schema::Nullable(x) => format!(
316            "NullableFieldRef::<{}>",
317            field_schema_to_reader_type(schema_set, &*x)?
318        ),
319        Schema::Boolean => "BooleanFieldRef".to_string(),
320        Schema::I32 => "I32FieldRef".to_string(),
321        Schema::I64 => "I64FieldRef".to_string(),
322        Schema::U32 => "U32FieldRef".to_string(),
323        Schema::U64 => "U64FieldRef".to_string(),
324        Schema::F32 => "F32FieldRef".to_string(),
325        Schema::F64 => "F64FieldRef".to_string(),
326        Schema::Bytes => "BytesFieldRef".to_string(),
327        Schema::String => "StringFieldRef".to_string(),
328        Schema::StaticArray(x) => format!(
329            "StaticArrayFieldRef::<{}>",
330            field_schema_to_reader_type(schema_set, x.item_type())?
331        ),
332        Schema::DynamicArray(x) => format!(
333            "DynamicArrayFieldRef::<{}>",
334            field_schema_to_reader_type(schema_set, x.item_type())?
335        ),
336        Schema::Map(x) => format!(
337            "MapFieldRef::<{}, {}>",
338            field_schema_to_reader_type(schema_set, x.key_type())?,
339            field_schema_to_reader_type(schema_set, x.value_type())?
340        ),
341        Schema::AssetRef(_x) => "AssetRefFieldRef".to_string(),
342        Schema::Record(x) | Schema::Enum(x) => {
343            let inner_type = schema_set.find_named_type_by_fingerprint(*x).unwrap();
344
345            match inner_type {
346                SchemaNamedType::Record(_) => format!("{}Ref", inner_type.name().to_string()),
347                SchemaNamedType::Enum(_) => {
348                    format!("EnumFieldRef::<{}Enum>", inner_type.name().to_string())
349                }
350            }
351        }
352    })
353}
354
355fn generate_reader(
356    schema_set: &SchemaSet,
357    schema: &SchemaRecord,
358) -> codegen::Scope {
359    let mut scope = codegen::Scope::new();
360
361    let record_name = format!("{}Ref<'a>", schema.name());
362    let record_name_without_generic = format!("{}Ref", schema.name());
363    let s = scope
364        .new_struct(record_name.as_str())
365        .tuple_field("PropertyPath")
366        .tuple_field("DataContainerRef<'a>");
367    s.vis("pub");
368
369    let field_impl = scope
370        .new_impl(record_name.as_str())
371        .generic("'a")
372        .impl_trait("FieldRef<'a>");
373    let new_fn = field_impl
374        .new_fn("new")
375        .arg("property_path", "PropertyPath")
376        .arg("data_container", "DataContainerRef<'a>");
377    new_fn.ret("Self");
378    new_fn.line(format!(
379        "{}(property_path, data_container)",
380        record_name_without_generic
381    ));
382
383    let record_impl = scope
384        .new_impl(record_name.as_str())
385        .generic("'a")
386        .impl_trait("RecordRef");
387    let schema_name_fn = record_impl.new_fn("schema_name");
388    schema_name_fn.ret("&'static str");
389    schema_name_fn.line(format!("\"{}\"", schema.name()));
390
391    let main_impl = scope.new_impl(record_name.as_str()).generic("'a");
392    for field in schema.fields() {
393        let field_type = field_schema_to_reader_type(schema_set, field.field_schema());
394        if let Some(field_type) = field_type {
395            let field_access_fn = main_impl.new_fn(field.name());
396            field_access_fn.arg_ref_self();
397            field_access_fn.ret(&field_type);
398            field_access_fn.vis("pub");
399            field_access_fn.line(format!(
400                "{}::new(self.0.push(\"{}\"), self.1.clone())",
401                field_type,
402                field.name()
403            ));
404        }
405    }
406
407    scope
408}
409
410fn field_schema_to_writer_type(
411    schema_set: &SchemaSet,
412    field_schema: &Schema,
413) -> Option<String> {
414    Some(match field_schema {
415        Schema::Nullable(x) => format!(
416            "NullableFieldRefMut::<{}>",
417            field_schema_to_writer_type(schema_set, &*x)?
418        ),
419        Schema::Boolean => "BooleanFieldRefMut".to_string(),
420        Schema::I32 => "I32FieldRefMut".to_string(),
421        Schema::I64 => "I64FieldRefMut".to_string(),
422        Schema::U32 => "U32FieldRefMut".to_string(),
423        Schema::U64 => "U64FieldRefMut".to_string(),
424        Schema::F32 => "F32FieldRefMut".to_string(),
425        Schema::F64 => "F64FieldRefMut".to_string(),
426        Schema::Bytes => "BytesFieldRefMut".to_string(),
427        Schema::String => "StringFieldRefMut".to_string(),
428        Schema::StaticArray(x) => format!(
429            "StaticArrayFieldRefMut::<{}>",
430            field_schema_to_writer_type(schema_set, x.item_type())?
431        ),
432        Schema::DynamicArray(x) => format!(
433            "DynamicArrayFieldRefMut::<{}>",
434            field_schema_to_writer_type(schema_set, x.item_type())?
435        ),
436        Schema::Map(x) => format!(
437            "MapFieldRefMut::<{}, {}>",
438            field_schema_to_writer_type(schema_set, x.key_type())?,
439            field_schema_to_writer_type(schema_set, x.value_type())?
440        ),
441        Schema::AssetRef(_x) => "AssetRefFieldRefMut".to_string(),
442        Schema::Record(x) | Schema::Enum(x) => {
443            let inner_type = schema_set.find_named_type_by_fingerprint(*x).unwrap();
444
445            match inner_type {
446                SchemaNamedType::Record(_) => format!("{}RefMut", inner_type.name().to_string()),
447                SchemaNamedType::Enum(_) => {
448                    format!("EnumFieldRefMut::<{}Enum>", inner_type.name().to_string())
449                }
450            }
451        }
452    })
453}
454
455fn generate_writer(
456    schema_set: &SchemaSet,
457    schema: &SchemaRecord,
458) -> codegen::Scope {
459    let mut scope = codegen::Scope::new();
460
461    let record_name = format!("{}RefMut<'a>", schema.name());
462    let record_name_without_generic = format!("{}RefMut", schema.name());
463    let s = scope
464        .new_struct(record_name.as_str())
465        .tuple_field("PropertyPath")
466        .tuple_field("Rc<RefCell<DataContainerRefMut<'a>>>");
467    s.vis("pub");
468
469    let field_impl = scope
470        .new_impl(record_name.as_str())
471        .generic("'a")
472        .impl_trait("FieldRefMut<'a>");
473    let new_fn = field_impl
474        .new_fn("new")
475        .arg("property_path", "PropertyPath")
476        .arg("data_container", "&Rc<RefCell<DataContainerRefMut<'a>>>");
477    new_fn.ret("Self");
478    new_fn.line(format!(
479        "{}(property_path, data_container.clone())",
480        record_name_without_generic
481    ));
482
483    let record_impl = scope
484        .new_impl(record_name.as_str())
485        .generic("'a")
486        .impl_trait("RecordRefMut");
487    let schema_name_fn = record_impl.new_fn("schema_name");
488    schema_name_fn.ret("&'static str");
489    schema_name_fn.line(format!("\"{}\"", schema.name()));
490
491    let main_impl = scope.new_impl(record_name.as_str()).generic("'a");
492    for field in schema.fields() {
493        let field_type = field_schema_to_writer_type(schema_set, field.field_schema());
494        if let Some(field_type) = field_type {
495            let field_access_fn = main_impl.new_fn(field.name());
496            //field_access_fn.arg_ref_self();
497            field_access_fn.arg("self", "&'a Self");
498            field_access_fn.ret(&field_type);
499            field_access_fn.vis("pub");
500            field_access_fn.line(format!(
501                "{}::new(self.0.push(\"{}\"), &self.1)",
502                field_type,
503                field.name()
504            ));
505        }
506    }
507
508    scope
509}
510
511fn field_schema_to_owned_type(
512    schema_set: &SchemaSet,
513    field_schema: &Schema,
514) -> Option<String> {
515    Some(match field_schema {
516        Schema::Nullable(x) => format!(
517            "NullableField::<{}>",
518            field_schema_to_owned_type(schema_set, &*x)?
519        ),
520        Schema::Boolean => "BooleanField".to_string(),
521        Schema::I32 => "I32Field".to_string(),
522        Schema::I64 => "I64Field".to_string(),
523        Schema::U32 => "U32Field".to_string(),
524        Schema::U64 => "U64Field".to_string(),
525        Schema::F32 => "F32Field".to_string(),
526        Schema::F64 => "F64Field".to_string(),
527        Schema::Bytes => "BytesField".to_string(),
528        Schema::String => "StringField".to_string(),
529        Schema::StaticArray(x) => format!(
530            "StaticArrayField::<{}>",
531            field_schema_to_owned_type(schema_set, x.item_type())?
532        ),
533        Schema::DynamicArray(x) => format!(
534            "DynamicArrayField::<{}>",
535            field_schema_to_owned_type(schema_set, x.item_type())?
536        ),
537        Schema::Map(x) => format!(
538            "MapField::<{}, {}>",
539            field_schema_to_owned_type(schema_set, x.key_type())?,
540            field_schema_to_owned_type(schema_set, x.value_type())?,
541        ),
542        Schema::AssetRef(_x) => "AssetRefField".to_string(),
543        Schema::Record(x) | Schema::Enum(x) => {
544            let inner_type = schema_set.find_named_type_by_fingerprint(*x).unwrap();
545
546            match inner_type {
547                SchemaNamedType::Record(_) => format!("{}Record", inner_type.name().to_string()),
548                SchemaNamedType::Enum(_) => {
549                    format!("EnumField::<{}Enum>", inner_type.name().to_string())
550                }
551            }
552        }
553    })
554}
555
556fn generate_owned(
557    schema_set: &SchemaSet,
558    schema: &SchemaRecord,
559) -> codegen::Scope {
560    let mut scope = codegen::Scope::new();
561
562    let record_name = format!("{}Record", schema.name());
563    let record_name_without_generic = format!("{}Record", schema.name());
564    let s = scope
565        .new_struct(record_name.as_str())
566        .tuple_field("PropertyPath")
567        .tuple_field("Rc<RefCell<Option<DataContainer>>>");
568    s.vis("pub");
569
570    let field_impl = scope.new_impl(record_name.as_str()).impl_trait("Field");
571    let new_fn = field_impl
572        .new_fn("new")
573        .arg("property_path", "PropertyPath")
574        .arg("data_container", "&Rc<RefCell<Option<DataContainer>>>");
575    new_fn.ret("Self");
576    new_fn.line(format!(
577        "{}(property_path, data_container.clone())",
578        record_name_without_generic
579    ));
580
581    let record_impl = scope.new_impl(record_name.as_str()).impl_trait("Record");
582
583    record_impl.associate_type("Reader<'a>", format!("{}Ref<'a>", schema.name()));
584    record_impl.associate_type("Writer<'a>", format!("{}RefMut<'a>", schema.name()));
585    record_impl.associate_type("Accessor", format!("{}Accessor", schema.name()));
586
587    let schema_name_fn = record_impl.new_fn("schema_name");
588    schema_name_fn.ret("&'static str");
589    schema_name_fn.line(format!("\"{}\"", schema.name()));
590
591    let main_impl = scope.new_impl(record_name.as_str());
592    for field in schema.fields() {
593        let field_type = field_schema_to_owned_type(schema_set, field.field_schema());
594        if let Some(field_type) = field_type {
595            let field_access_fn = main_impl.new_fn(field.name());
596            //field_access_fn.arg_ref_self();
597            field_access_fn.arg("self", "&Self");
598            field_access_fn.ret(&field_type);
599            field_access_fn.vis("pub");
600            field_access_fn.line(format!(
601                "{}::new(self.0.push(\"{}\"), &self.1)",
602                field_type,
603                field.name()
604            ));
605        }
606    }
607
608    scope
609}