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#[derive(StructOpt, Debug, Default)]
17pub struct HydrateCodegenArgs {
18 #[structopt(name = "job-name", long)]
22 pub job_name: Option<String>,
23
24 #[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 let project_configuration =
54 HydrateProjectConfiguration::locate_project_file(project_file_serach_location).unwrap();
55
56 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 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 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 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 code_fragments_as_string.push(code_fragment_as_string);
139 }
140 }
141
142 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("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("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}