1use crate::{browser, browser::Browser, Session};
2use std::collections::BTreeMap;
3use std::error::Error as StdError;
4use std::fmt;
5use std::fs;
6use std::io;
7use std::path::{Path, PathBuf};
8
9pub type Result<T> = std::result::Result<T, Error>;
10
11pub const DEFAULT_CONFIG_PATH: &str = "gemstone-rs.codegen";
12pub const DEFAULT_OUTPUT_PATH: &str = "src/generated/gemstone_wrappers.rs";
13
14#[derive(Debug)]
15pub enum Error {
16 Io(io::Error),
17 GemStone(crate::Error),
18 Config {
19 path: Option<PathBuf>,
20 line: usize,
21 message: String,
22 },
23}
24
25impl Error {
26 fn config(path: Option<&Path>, line: usize, message: impl Into<String>) -> Self {
27 Self::Config {
28 path: path.map(Path::to_path_buf),
29 line,
30 message: message.into(),
31 }
32 }
33}
34
35impl fmt::Display for Error {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::Io(err) => write!(f, "{err}"),
39 Self::GemStone(err) => write!(f, "{err}"),
40 Self::Config {
41 path,
42 line,
43 message,
44 } => {
45 if let Some(path) = path {
46 write!(f, "{}:{line}: {message}", path.display())
47 } else {
48 write!(f, "line {line}: {message}")
49 }
50 }
51 }
52 }
53}
54
55impl StdError for Error {
56 fn source(&self) -> Option<&(dyn StdError + 'static)> {
57 match self {
58 Self::Io(err) => Some(err),
59 Self::GemStone(err) => Some(err),
60 Self::Config { .. } => None,
61 }
62 }
63}
64
65impl From<io::Error> for Error {
66 fn from(value: io::Error) -> Self {
67 Self::Io(value)
68 }
69}
70
71impl From<crate::Error> for Error {
72 fn from(value: crate::Error) -> Self {
73 Self::GemStone(value)
74 }
75}
76
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct Config {
79 pub output: PathBuf,
80 pub classes: Vec<ClassSpec>,
81 pub mapped: Vec<MappedSpec>,
82}
83
84impl Config {
85 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
86 let path = path.as_ref();
87 let source = fs::read_to_string(path)?;
88 Self::parse(&source, Some(path))
89 }
90
91 pub fn parse(source: &str, path: Option<&Path>) -> Result<Self> {
92 let base_dir = path
93 .and_then(Path::parent)
94 .filter(|parent| !parent.as_os_str().is_empty())
95 .unwrap_or_else(|| Path::new("."));
96 let mut output = PathBuf::from(DEFAULT_OUTPUT_PATH);
97 let mut classes: BTreeMap<ClassRef, ClassSpec> = BTreeMap::new();
98 let mut mapped: BTreeMap<String, MappedSpec> = BTreeMap::new();
99
100 for (index, raw_line) in source.lines().enumerate() {
101 let line_no = index + 1;
102 let line = raw_line.trim();
103 if line.is_empty() || line.starts_with('#') {
104 continue;
105 }
106
107 let (key, value) = split_directive(line)
108 .ok_or_else(|| Error::config(path, line_no, "expected key=value or key value"))?;
109 match key {
110 "output" => {
111 if value.is_empty() {
112 return Err(Error::config(path, line_no, "output path is empty"));
113 }
114 output = PathBuf::from(value);
115 }
116 "class" => {
117 let class_ref = ClassRef::parse(value)
118 .map_err(|message| Error::config(path, line_no, message))?;
119 classes
120 .entry(class_ref.clone())
121 .or_insert_with(|| ClassSpec::new(class_ref));
122 }
123 "method" => {
124 let method = MethodSpec::parse(value)
125 .map_err(|message| Error::config(path, line_no, message))?;
126 classes
127 .entry(method.class_ref.clone())
128 .or_insert_with(|| ClassSpec::new(method.class_ref.clone()))
129 .methods
130 .push(method);
131 }
132 "mapped" => {
133 let spec = MappedSpec::parse(value)
134 .map_err(|message| Error::config(path, line_no, message))?;
135 mapped.entry(spec.name.clone()).or_insert(spec);
136 }
137 "field" => {
138 let field = FieldSpec::parse(value)
139 .map_err(|message| Error::config(path, line_no, message))?;
140 mapped
141 .entry(field.mapped_name.clone())
142 .or_insert_with(|| MappedSpec::new(field.mapped_name.clone()))
143 .fields
144 .push(field);
145 }
146 other => {
147 return Err(Error::config(
148 path,
149 line_no,
150 format!("unknown directive: {other}"),
151 ));
152 }
153 }
154 }
155
156 if output.is_relative() {
157 output = base_dir.join(output);
158 }
159
160 Ok(Self {
161 output,
162 classes: classes.into_values().collect(),
163 mapped: mapped.into_values().collect(),
164 })
165 }
166}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct ClassSpec {
170 pub class_ref: ClassRef,
171 pub methods: Vec<MethodSpec>,
172}
173
174impl ClassSpec {
175 fn new(class_ref: ClassRef) -> Self {
176 Self {
177 class_ref,
178 methods: Vec::new(),
179 }
180 }
181}
182
183#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
184pub struct ClassRef {
185 pub class_name: String,
186 pub dictionary: String,
187 pub meta: bool,
188}
189
190impl ClassRef {
191 pub fn parse(value: &str) -> std::result::Result<Self, String> {
192 let mut text = value.trim();
193 if text.is_empty() {
194 return Err("class reference is empty".to_string());
195 }
196
197 let meta = text.ends_with(" class");
198 if meta {
199 text = text.trim_end_matches(" class").trim_end();
200 }
201
202 let (dictionary, class_name) = text
203 .split_once(':')
204 .map(|(dictionary, class_name)| (dictionary.trim(), class_name.trim()))
205 .unwrap_or(("", text));
206
207 if class_name.is_empty() {
208 return Err("class name is empty".to_string());
209 }
210
211 Ok(Self {
212 class_name: class_name.to_string(),
213 dictionary: dictionary.to_string(),
214 meta,
215 })
216 }
217
218 pub fn display_name(&self) -> String {
219 let class_name = if self.dictionary.is_empty() {
220 self.class_name.clone()
221 } else {
222 format!("{}:{}", self.dictionary, self.class_name)
223 };
224 if self.meta {
225 format!("{class_name} class")
226 } else {
227 class_name
228 }
229 }
230
231 fn struct_name(&self) -> String {
232 let mut name = rust_type_name(&self.class_name);
233 if self.meta {
234 name.push_str("Class");
235 }
236 name
237 }
238}
239
240#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct MappedSpec {
242 pub name: String,
243 pub fields: Vec<FieldSpec>,
244 pub doc: Option<String>,
245}
246
247impl MappedSpec {
248 fn new(name: String) -> Self {
249 Self {
250 name,
251 fields: Vec::new(),
252 doc: None,
253 }
254 }
255
256 pub fn parse(value: &str) -> std::result::Result<Self, String> {
257 let mut parts = value.split('|').map(str::trim);
258 let name = parts.next().unwrap_or_default().trim();
259 if name.is_empty() {
260 return Err("mapped struct name is empty".to_string());
261 }
262 let mut spec = Self::new(rust_type_name(name));
263 for option in parts {
264 let Some((key, value)) = option.split_once('=') else {
265 return Err(format!("mapped option must look like key=value: {option}"));
266 };
267 match key.trim() {
268 "doc" => spec.doc = Some(value.trim().to_string()),
269 other => return Err(format!("unknown mapped option: {other}")),
270 }
271 }
272 Ok(spec)
273 }
274}
275
276#[derive(Clone, Debug, Eq, PartialEq)]
277pub struct FieldSpec {
278 pub mapped_name: String,
279 pub rust_name: String,
280 pub key: String,
281 pub key_type: FieldKeyType,
282 pub field_type: FieldType,
283 pub doc: Option<String>,
284}
285
286impl FieldSpec {
287 pub fn parse(value: &str) -> std::result::Result<Self, String> {
288 let mut parts = value.split('|').map(str::trim);
289 let head = parts
290 .next()
291 .ok_or_else(|| "field must look like MappedStruct.field".to_string())?;
292 let (mapped_name, rust_name) = head
293 .split_once('.')
294 .ok_or_else(|| "field must look like MappedStruct.field".to_string())?;
295 let mapped_name = rust_type_name(mapped_name.trim());
296 let rust_name = rust_fn_name(rust_name.trim());
297 if mapped_name.is_empty() || rust_name.is_empty() {
298 return Err("field mapping has an empty struct or field name".to_string());
299 }
300 let mut key = rust_name.clone();
301 let mut key_type = FieldKeyType::String;
302 let mut field_type = FieldType::String;
303 let mut doc = None;
304 for option in parts {
305 let Some((option_key, value)) = option.split_once('=') else {
306 return Err(format!("field option must look like key=value: {option}"));
307 };
308 match option_key.trim() {
309 "key" => key = value.trim().to_string(),
310 "key_type" | "keyType" => key_type = FieldKeyType::parse(value.trim())?,
311 "type" => field_type = FieldType::parse(value.trim())?,
312 "doc" => doc = Some(value.trim().to_string()),
313 other => return Err(format!("unknown field option: {other}")),
314 }
315 }
316 Ok(Self {
317 mapped_name,
318 rust_name,
319 key,
320 key_type,
321 field_type,
322 doc,
323 })
324 }
325}
326
327#[derive(Clone, Debug, Eq, PartialEq)]
328pub enum FieldKeyType {
329 String,
330 Symbol,
331}
332
333impl FieldKeyType {
334 fn parse(value: &str) -> std::result::Result<Self, String> {
335 match value {
336 "" | "String" | "string" | "str" => Ok(Self::String),
337 "Symbol" | "symbol" => Ok(Self::Symbol),
338 other => Err(format!("unsupported key_type: {other}")),
339 }
340 }
341
342 fn config_name(&self) -> &'static str {
343 match self {
344 Self::String => "String",
345 Self::Symbol => "Symbol",
346 }
347 }
348
349 fn bridge_source(&self) -> &'static str {
350 match self {
351 Self::String => "BridgeKeyType::String",
352 Self::Symbol => "BridgeKeyType::Symbol",
353 }
354 }
355}
356
357#[derive(Clone, Debug, Eq, PartialEq)]
358pub enum FieldType {
359 String,
360 SmallInt,
361 Bool,
362 Oop,
363 Mapped(String),
364 Vec(Box<FieldType>),
365}
366
367impl FieldType {
368 fn parse(value: &str) -> std::result::Result<Self, String> {
369 if let Some(inner) = value
370 .strip_prefix("Vec<")
371 .and_then(|text| text.strip_suffix('>'))
372 {
373 return Ok(Self::Vec(Box::new(Self::parse(inner.trim())?)));
374 }
375 if let Some(inner) = value
376 .strip_prefix("Array<")
377 .and_then(|text| text.strip_suffix('>'))
378 {
379 return Ok(Self::Vec(Box::new(Self::parse(inner.trim())?)));
380 }
381 if let Some(inner) = value
382 .strip_prefix("Mapped<")
383 .and_then(|text| text.strip_suffix('>'))
384 {
385 return Ok(Self::Mapped(rust_type_name(inner.trim())));
386 }
387 if let Some(inner) = value
388 .strip_prefix("Mapped(")
389 .and_then(|text| text.strip_suffix(')'))
390 {
391 return Ok(Self::Mapped(rust_type_name(inner.trim())));
392 }
393 match value {
394 "" | "String" | "string" => Ok(Self::String),
395 "SmallInt" | "smallInt" | "smallint" | "i64" => Ok(Self::SmallInt),
396 "Bool" | "Boolean" | "bool" | "boolean" => Ok(Self::Bool),
397 "Oop" | "OOP" | "oop" => Ok(Self::Oop),
398 other => {
399 if other
400 .chars()
401 .next()
402 .is_some_and(|ch| ch.is_ascii_uppercase())
403 {
404 Ok(Self::Mapped(rust_type_name(other)))
405 } else {
406 Err(format!("unsupported field type: {other}"))
407 }
408 }
409 }
410 }
411
412 fn rust_type(&self) -> String {
413 match self {
414 Self::String => "String".to_string(),
415 Self::SmallInt => "i64".to_string(),
416 Self::Bool => "bool".to_string(),
417 Self::Oop => "Oop".to_string(),
418 Self::Mapped(name) => name.clone(),
419 Self::Vec(inner) => format!("Vec<{}>", inner.rust_type()),
420 }
421 }
422
423 fn config_name(&self) -> String {
424 match self {
425 Self::String => "String".to_string(),
426 Self::SmallInt => "SmallInt".to_string(),
427 Self::Bool => "Bool".to_string(),
428 Self::Oop => "Oop".to_string(),
429 Self::Mapped(name) => format!("Mapped<{name}>"),
430 Self::Vec(inner) => format!("Vec<{}>", inner.config_name()),
431 }
432 }
433}
434
435#[derive(Clone, Debug, Eq, PartialEq)]
436pub struct MethodSpec {
437 pub class_ref: ClassRef,
438 pub selector: String,
439 pub args: Vec<String>,
440 pub return_type: ReturnType,
441 pub doc: Option<String>,
442}
443
444impl MethodSpec {
445 pub fn parse(value: &str) -> std::result::Result<Self, String> {
446 let mut parts = value.split('|').map(str::trim);
447 let head = parts
448 .next()
449 .ok_or_else(|| "method must look like Class>>selector".to_string())?;
450 let (class_ref, selector) = head
451 .split_once(">>")
452 .ok_or_else(|| "method must look like Class>>selector".to_string())?;
453 let class_ref = ClassRef::parse(class_ref)?;
454 let selector = selector.trim();
455 if selector.is_empty() {
456 return Err("method selector is empty".to_string());
457 }
458 let mut args = Vec::new();
459 let mut return_type = ReturnType::Value;
460 let mut doc = None;
461 for option in parts {
462 let Some((key, value)) = option.split_once('=') else {
463 return Err(format!("method option must look like key=value: {option}"));
464 };
465 match key.trim() {
466 "args" => {
467 args = value
468 .split(',')
469 .map(str::trim)
470 .filter(|arg| !arg.is_empty())
471 .map(str::to_string)
472 .collect();
473 }
474 "return" => return_type = ReturnType::parse(value.trim())?,
475 "doc" => doc = Some(value.trim().to_string()),
476 other => return Err(format!("unknown method option: {other}")),
477 }
478 }
479 let selector_arg_count = selector.matches(':').count();
480 if !args.is_empty() && args.len() != selector_arg_count {
481 return Err(format!(
482 "selector {selector} expects {selector_arg_count} arguments, got {} names",
483 args.len()
484 ));
485 }
486 Ok(Self {
487 class_ref,
488 selector: selector.to_string(),
489 args,
490 return_type,
491 doc,
492 })
493 }
494
495 fn fn_name(&self) -> String {
496 rust_fn_name(&self.selector)
497 }
498
499 fn arg_count(&self) -> usize {
500 self.selector.matches(':').count()
501 }
502
503 fn arg_names(&self) -> Vec<String> {
504 if self.args.is_empty() {
505 (0..self.arg_count())
506 .map(|index| format!("arg{index}"))
507 .collect()
508 } else {
509 self.args.iter().map(|arg| rust_fn_name(arg)).collect()
510 }
511 }
512}
513
514#[derive(Clone, Debug, Eq, PartialEq)]
515pub enum ReturnType {
516 Value,
517 String,
518 SmallInt,
519 Bool,
520 Oop,
521}
522
523impl ReturnType {
524 fn parse(value: &str) -> std::result::Result<Self, String> {
525 match value {
526 "" | "Value" | "value" => Ok(Self::Value),
527 "String" | "string" => Ok(Self::String),
528 "SmallInt" | "smallInt" | "smallint" | "i64" => Ok(Self::SmallInt),
529 "Bool" | "Boolean" | "bool" | "boolean" => Ok(Self::Bool),
530 "Oop" | "OOP" | "oop" => Ok(Self::Oop),
531 other => Err(format!("unsupported return type: {other}")),
532 }
533 }
534
535 fn config_name(&self) -> &'static str {
536 match self {
537 Self::Value => "Value",
538 Self::String => "String",
539 Self::SmallInt => "SmallInt",
540 Self::Bool => "Bool",
541 Self::Oop => "Oop",
542 }
543 }
544
545 fn rust_type(&self) -> &'static str {
546 match self {
547 Self::Value => "Value",
548 Self::String => "String",
549 Self::SmallInt => "i64",
550 Self::Bool => "bool",
551 Self::Oop => "Oop",
552 }
553 }
554}
555
556#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct GeneratedCode {
558 pub output: PathBuf,
559 pub source: String,
560}
561
562#[derive(Clone, Debug, Eq, PartialEq)]
563pub struct CheckReport {
564 pub output: PathBuf,
565 pub exists: bool,
566 pub up_to_date: bool,
567}
568
569#[derive(Clone, Debug, Eq, PartialEq)]
570pub struct DiffReport {
571 pub output: PathBuf,
572 pub exists: bool,
573 pub up_to_date: bool,
574 pub diff: String,
575}
576
577pub fn load_or_sample(path: impl AsRef<Path>) -> Result<Config> {
578 let path = path.as_ref();
579 if path.exists() {
580 Config::from_file(path)
581 } else {
582 Config::parse(sample_config(), Some(path))
583 }
584}
585
586pub fn generate(config: &Config) -> GeneratedCode {
587 GeneratedCode {
588 output: config.output.clone(),
589 source: generate_source(config),
590 }
591}
592
593pub fn generate_to_file(config: &Config) -> Result<GeneratedCode> {
594 let generated = generate(config);
595 if let Some(parent) = generated.output.parent() {
596 if !parent.as_os_str().is_empty() {
597 fs::create_dir_all(parent)?;
598 }
599 }
600 fs::write(&generated.output, &generated.source)?;
601 Ok(generated)
602}
603
604pub fn write_config(path: impl AsRef<Path>, config: &Config) -> Result<()> {
605 let path = path.as_ref();
606 if let Some(parent) = path.parent() {
607 if !parent.as_os_str().is_empty() {
608 fs::create_dir_all(parent)?;
609 }
610 }
611 fs::write(path, config_source(config))?;
612 Ok(())
613}
614
615pub fn check(config: &Config) -> Result<CheckReport> {
616 let generated = generate(config);
617 match fs::read_to_string(&generated.output) {
618 Ok(current) => Ok(CheckReport {
619 output: generated.output,
620 exists: true,
621 up_to_date: current == generated.source,
622 }),
623 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(CheckReport {
624 output: generated.output,
625 exists: false,
626 up_to_date: false,
627 }),
628 Err(err) => Err(Error::Io(err)),
629 }
630}
631
632pub fn diff(config: &Config) -> Result<DiffReport> {
633 let generated = generate(config);
634 match fs::read_to_string(&generated.output) {
635 Ok(current) => {
636 let up_to_date = current == generated.source;
637 let diff = if up_to_date {
638 String::new()
639 } else {
640 simple_diff(&generated.output, ¤t, &generated.source)
641 };
642 Ok(DiffReport {
643 output: generated.output,
644 exists: true,
645 up_to_date,
646 diff,
647 })
648 }
649 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(DiffReport {
650 output: generated.output.clone(),
651 exists: false,
652 up_to_date: false,
653 diff: simple_diff(&generated.output, "", &generated.source),
654 }),
655 Err(err) => Err(Error::Io(err)),
656 }
657}
658
659pub fn discover(session: &mut Session, output: PathBuf, classes: &[ClassRef]) -> Result<Config> {
660 let classes = if classes.is_empty() {
661 vec![ClassRef::parse("Object").map_err(|message| Error::config(None, 0, message))?]
662 } else {
663 classes.to_vec()
664 };
665 let mut browser = Browser::new(session);
666 let mut specs = Vec::new();
667 for class_ref in classes {
668 let selectors = browser.methods(
669 &class_ref.class_name,
670 browser::ALL_PROTOCOLS,
671 class_ref.meta,
672 &class_ref.dictionary,
673 )?;
674 let mut spec = ClassSpec::new(class_ref.clone());
675 for selector in selectors {
676 let source = browser
677 .source(
678 &class_ref.class_name,
679 &selector,
680 class_ref.meta,
681 &class_ref.dictionary,
682 )
683 .unwrap_or_default();
684 spec.methods.push(MethodSpec {
685 class_ref: class_ref.clone(),
686 selector,
687 args: Vec::new(),
688 return_type: ReturnType::Value,
689 doc: first_source_line(&source),
690 });
691 }
692 specs.push(spec);
693 }
694 Ok(Config {
695 output,
696 classes: specs,
697 mapped: Vec::new(),
698 })
699}
700
701pub fn discover_mapping(
702 session: &mut Session,
703 output: PathBuf,
704 mapped_name: &str,
705 class_ref: &ClassRef,
706) -> Result<Config> {
707 let class_oop = session.execute(&browser::behavior_expr(
708 &class_ref.class_name,
709 class_ref.meta,
710 &class_ref.dictionary,
711 ))?;
712 let names_oop = session.perform_oop(class_oop, "allInstVarNames", &[])?;
713 let mut fields = Vec::new();
714 for name in session.array_strings(names_oop)? {
715 let rust_name = rust_fn_name(&name);
716 fields.push(FieldSpec {
717 mapped_name: rust_type_name(mapped_name),
718 rust_name,
719 key: name,
720 key_type: FieldKeyType::Symbol,
721 field_type: FieldType::String,
722 doc: Some("Discovered from GemStone instance variable name.".to_string()),
723 });
724 }
725 if fields.is_empty() {
726 fields.push(FieldSpec {
727 mapped_name: rust_type_name(mapped_name),
728 rust_name: "name".to_string(),
729 key: "name".to_string(),
730 key_type: FieldKeyType::String,
731 field_type: FieldType::String,
732 doc: Some("Placeholder field; edit after discovery.".to_string()),
733 });
734 }
735 Ok(Config {
736 output,
737 classes: Vec::new(),
738 mapped: vec![MappedSpec {
739 name: rust_type_name(mapped_name),
740 fields,
741 doc: Some(format!(
742 "Mapping proposal discovered from {}.",
743 class_ref.display_name()
744 )),
745 }],
746 })
747}
748
749pub fn config_source(config: &Config) -> String {
750 let mut source = String::new();
751 source.push_str("# gemstone-rs codegen config\n");
752 source.push_str("# Empty dictionary means resolve through the active user's symbol list.\n");
753 source.push_str(&format!("output = {}\n", config.output.display()));
754 for class in &config.classes {
755 source.push_str(&format!("class = {}\n", class.class_ref.display_name()));
756 for method in &class.methods {
757 source.push_str("method = ");
758 source.push_str(&method.class_ref.display_name());
759 source.push_str(">>");
760 source.push_str(&method.selector);
761 if !method.args.is_empty() {
762 source.push_str(" | args=");
763 source.push_str(&method.args.join(","));
764 }
765 if method.return_type != ReturnType::Value {
766 source.push_str(" | return=");
767 source.push_str(method.return_type.config_name());
768 }
769 if let Some(doc) = method.doc.as_deref().filter(|doc| !doc.is_empty()) {
770 source.push_str(" | doc=");
771 source.push_str(&doc.replace('\n', " "));
772 }
773 source.push('\n');
774 }
775 }
776 for mapped in &config.mapped {
777 source.push_str(&format!("mapped = {}", mapped.name));
778 if let Some(doc) = mapped.doc.as_deref().filter(|doc| !doc.is_empty()) {
779 source.push_str(" | doc=");
780 source.push_str(&doc.replace('\n', " "));
781 }
782 source.push('\n');
783 for field in &mapped.fields {
784 source.push_str(&format!(
785 "field = {}.{} | type={} | key={}",
786 mapped.name,
787 field.rust_name,
788 field.field_type.config_name(),
789 field.key
790 ));
791 if field.key_type != FieldKeyType::String {
792 source.push_str(" | key_type=");
793 source.push_str(field.key_type.config_name());
794 }
795 if let Some(doc) = field.doc.as_deref().filter(|doc| !doc.is_empty()) {
796 source.push_str(" | doc=");
797 source.push_str(&doc.replace('\n', " "));
798 }
799 source.push('\n');
800 }
801 }
802 source
803}
804
805pub fn sample_config() -> &'static str {
806 "# gemstone-rs codegen config\n\
807 # Empty dictionary means resolve through the active user's symbol list.\n\
808 output = src/generated/gemstone_wrappers.rs\n\
809 class = Object\n\
810 method = Object>>printString | return=String | doc=Return the receiver printString.\n\
811 method = Object>>class\n\
812 mapped = BookingDraft | doc=A typed Rust payload stored under BridgeRoot.\n\
813 field = BookingDraft.name | type=String | key=name\n\
814 field = BookingDraft.amount | type=SmallInt | key=amount\n\
815 field = BookingDraft.currency | type=String | key=currency\n\
816 field = BookingDraft.tags | type=Vec<String> | key=tags\n"
817}
818
819fn generate_source(config: &Config) -> String {
820 let mut source = String::new();
821 source.push_str("// @generated by gemstone-rs codegen. Do not edit by hand.\n");
822 source.push_str(
823 "use gemstone_rs::{\n BridgeDictionary, BridgeFieldRead, BridgeFieldWrite, BridgeKey, BridgeKeyType, BridgeMapped,\n BridgeValue, Error, Oop, Result, Session, Value,\n};\n\n",
824 );
825
826 for class in &config.classes {
827 let struct_name = class.class_ref.struct_name();
828 source.push_str(&format!("pub struct {struct_name}<'a> {{\n"));
829 source.push_str(" session: &'a mut Session,\n");
830 source.push_str(" oop: Oop,\n");
831 source.push_str("}\n\n");
832 source.push_str(&format!("impl<'a> {struct_name}<'a> {{\n"));
833 source.push_str(" pub fn resolve(session: &'a mut Session) -> Result<Self> {\n");
834 source.push_str(" let oop =\n");
835 source.push_str(&format!(
836 " session.execute({})?;\n",
837 rust_string_literal(&browser::behavior_expr(
838 &class.class_ref.class_name,
839 class.class_ref.meta,
840 &class.class_ref.dictionary,
841 ))
842 ));
843 source.push_str(" Ok(Self { session, oop })\n");
844 source.push_str(" }\n\n");
845 source.push_str(" pub fn from_oop(session: &'a mut Session, oop: Oop) -> Self {\n");
846 source.push_str(" Self { session, oop }\n");
847 source.push_str(" }\n\n");
848 source.push_str(" pub fn oop(&self) -> Oop {\n");
849 source.push_str(" self.oop\n");
850 source.push_str(" }\n");
851
852 for method in &class.methods {
853 source.push('\n');
854 source.push_str(&method_source(method));
855 }
856
857 source.push_str("}\n\n");
858 }
859
860 for mapped in &config.mapped {
861 source.push_str(&mapped_source(mapped));
862 source.push('\n');
863 }
864
865 while source.ends_with("\n\n") {
866 source.pop();
867 }
868 source
869}
870
871fn mapped_source(mapped: &MappedSpec) -> String {
872 let mut source = String::new();
873 if let Some(doc) = mapped.doc.as_deref().filter(|doc| !doc.is_empty()) {
874 source.push_str(&format!("/// {}\n", escape_doc(doc)));
875 }
876 source.push_str("#[derive(Clone, Debug, Eq, PartialEq)]\n");
877 source.push_str(&format!("pub struct {} {{\n", mapped.name));
878 for field in &mapped.fields {
879 if let Some(doc) = field.doc.as_deref().filter(|doc| !doc.is_empty()) {
880 source.push_str(&format!(" /// {}\n", escape_doc(doc)));
881 }
882 source.push_str(&format!(
883 " pub {}: {},\n",
884 field.rust_name,
885 field.field_type.rust_type()
886 ));
887 }
888 source.push_str("}\n\n");
889 source.push_str(&format!("impl BridgeMapped for {} {{\n", mapped.name));
890 source.push_str(" fn to_bridge_value(&self) -> BridgeValue {\n");
891 source.push_str(" BridgeValue::keyed_dictionary([\n");
892 for field in &mapped.fields {
893 source.push_str(&mapped_field_write(field));
894 }
895 source.push_str(" ])\n");
896 source.push_str(" }\n\n");
897 source.push_str(
898 " fn from_bridge_dictionary(dictionary: &mut BridgeDictionary<'_>) -> Result<Self> {\n",
899 );
900 source.push_str(" Ok(Self {\n");
901 for field in &mapped.fields {
902 source.push_str(&mapped_field_read(field));
903 }
904 source.push_str(" })\n");
905 source.push_str(" }\n");
906 source.push_str("}\n");
907 source
908}
909
910fn mapped_field_write(field: &FieldSpec) -> String {
911 format!(
912 " (\n BridgeKey::new({}, {}),\n BridgeFieldWrite::to_bridge_field_value(&self.{}),\n ),\n",
913 rust_string_literal(&field.key),
914 field.key_type.bridge_source(),
915 field.rust_name
916 )
917}
918
919fn mapped_field_read(field: &FieldSpec) -> String {
920 let inline = format!(
921 " {}: BridgeFieldRead::read_bridge_field(dictionary, {}, {})?,\n",
922 field.rust_name,
923 rust_string_literal(&field.key),
924 field.key_type.bridge_source()
925 );
926 if inline.trim_end().len() <= 100 {
927 return inline;
928 }
929 format!(
930 " {}: BridgeFieldRead::read_bridge_field(\n dictionary,\n {},\n {},\n )?,\n",
931 field.rust_name,
932 rust_string_literal(&field.key),
933 field.key_type.bridge_source()
934 )
935}
936
937fn method_source(method: &MethodSpec) -> String {
938 let mut source = String::new();
939 let fn_name = method.fn_name();
940 let arg_names = method.arg_names();
941 let args: Vec<String> = arg_names.iter().map(|arg| format!("{arg}: Oop")).collect();
942 let args_suffix = if args.is_empty() {
943 String::new()
944 } else {
945 format!(", {}", args.join(", "))
946 };
947 if let Some(doc) = method.doc.as_deref().filter(|doc| !doc.is_empty()) {
948 source.push_str(&format!(" /// {}\n", escape_doc(doc)));
949 }
950 source.push_str(&format!(
951 " pub fn {fn_name}(&mut self{args_suffix}) -> Result<{}> {{\n",
952 method.return_type.rust_type()
953 ));
954 source.push_str(&format!(
955 " let value = self.session.perform(self.oop, {}, &[{}])?;\n",
956 rust_string_literal(&method.selector),
957 arg_names.join(", ")
958 ));
959 source.push_str(&return_conversion(&method.return_type));
960 source.push_str(" }\n");
961 source
962}
963
964fn return_conversion(return_type: &ReturnType) -> String {
965 match return_type {
966 ReturnType::Value => " Ok(value)\n".to_string(),
967 ReturnType::String => typed_match(
968 "String",
969 &[
970 " Value::String(value) => Ok(value),",
971 " Value::Oop(oop) => self.session.fetch_string(oop),",
972 ],
973 ),
974 ReturnType::SmallInt => typed_match(
975 "SmallInt",
976 &[" Value::SmallInt(value) => Ok(value),"],
977 ),
978 ReturnType::Bool => typed_match("Bool", &[" Value::Bool(value) => Ok(value),"]),
979 ReturnType::Oop => typed_match("Oop", &[" Value::Oop(oop) => Ok(oop),"]),
980 }
981}
982
983fn typed_match(expected: &'static str, arms: &[&str]) -> String {
984 let mut source = String::from(" match value {\n");
985 for arm in arms {
986 source.push_str(arm);
987 source.push('\n');
988 }
989 source.push_str(" other => Err(Error::UnexpectedType {\n");
990 source.push_str(&format!(" expected: {expected:?},\n"));
991 source.push_str(" actual: format!(\"{other:?}\"),\n");
992 source.push_str(" }),\n");
993 source.push_str(" }\n");
994 source
995}
996
997fn split_directive(line: &str) -> Option<(&str, &str)> {
998 if let Some((key, value)) = line.split_once('=') {
999 return Some((key.trim(), value.trim()));
1000 }
1001 let mut parts = line.splitn(2, char::is_whitespace);
1002 let key = parts.next()?.trim();
1003 let value = parts.next()?.trim();
1004 Some((key, value))
1005}
1006
1007fn first_source_line(source: &str) -> Option<String> {
1008 source
1009 .lines()
1010 .map(str::trim)
1011 .find(|line| !line.is_empty())
1012 .map(|line| line.chars().take(120).collect())
1013}
1014
1015fn simple_diff(path: &Path, current: &str, generated: &str) -> String {
1016 let mut diff = String::new();
1017 diff.push_str(&format!("--- {}\n", path.display()));
1018 diff.push_str(&format!("+++ {} (generated)\n", path.display()));
1019 for line in current.lines() {
1020 diff.push('-');
1021 diff.push_str(line);
1022 diff.push('\n');
1023 }
1024 for line in generated.lines() {
1025 diff.push('+');
1026 diff.push_str(line);
1027 diff.push('\n');
1028 }
1029 diff
1030}
1031
1032fn escape_doc(value: &str) -> String {
1033 value.replace(['\r', '\n'], " ")
1034}
1035
1036fn rust_type_name(value: &str) -> String {
1037 let mut result = String::new();
1038 let mut capitalize = true;
1039 for ch in value.chars() {
1040 if ch.is_ascii_alphanumeric() {
1041 if capitalize {
1042 result.push(ch.to_ascii_uppercase());
1043 capitalize = false;
1044 } else {
1045 result.push(ch);
1046 }
1047 } else {
1048 capitalize = true;
1049 }
1050 }
1051 if result.is_empty() {
1052 result.push_str("GemStoneObject");
1053 }
1054 if result.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
1055 result.insert(0, 'G');
1056 }
1057 result
1058}
1059
1060fn rust_fn_name(selector: &str) -> String {
1061 let mut result = String::new();
1062 let mut previous_was_separator = true;
1063 for ch in selector.chars() {
1064 if ch.is_ascii_uppercase() {
1065 if !result.is_empty() && !previous_was_separator {
1066 result.push('_');
1067 }
1068 result.push(ch.to_ascii_lowercase());
1069 previous_was_separator = false;
1070 } else if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
1071 result.push(ch);
1072 previous_was_separator = false;
1073 } else if !result.ends_with('_') && !result.is_empty() {
1074 result.push('_');
1075 previous_was_separator = true;
1076 }
1077 }
1078 while result.ends_with('_') {
1079 result.pop();
1080 }
1081 if result.is_empty() {
1082 result.push_str("perform");
1083 }
1084 if result.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
1085 result.insert(0, '_');
1086 }
1087 if is_rust_keyword(&result) {
1088 result.push('_');
1089 }
1090 result
1091}
1092
1093fn rust_string_literal(value: &str) -> String {
1094 let mut result = String::from("\"");
1095 for ch in value.chars() {
1096 match ch {
1097 '"' => result.push_str("\\\""),
1098 '\\' => result.push_str("\\\\"),
1099 '\n' => result.push_str("\\n"),
1100 '\r' => result.push_str("\\r"),
1101 '\t' => result.push_str("\\t"),
1102 ch if ch.is_control() => result.push_str(&format!("\\u{{{:x}}}", ch as u32)),
1103 ch => result.push(ch),
1104 }
1105 }
1106 result.push('"');
1107 result
1108}
1109
1110fn is_rust_keyword(value: &str) -> bool {
1111 matches!(
1112 value,
1113 "as" | "break"
1114 | "const"
1115 | "continue"
1116 | "crate"
1117 | "else"
1118 | "enum"
1119 | "extern"
1120 | "false"
1121 | "fn"
1122 | "for"
1123 | "if"
1124 | "impl"
1125 | "in"
1126 | "let"
1127 | "loop"
1128 | "match"
1129 | "mod"
1130 | "move"
1131 | "mut"
1132 | "pub"
1133 | "ref"
1134 | "return"
1135 | "self"
1136 | "Self"
1137 | "static"
1138 | "struct"
1139 | "super"
1140 | "trait"
1141 | "true"
1142 | "type"
1143 | "unsafe"
1144 | "use"
1145 | "where"
1146 | "while"
1147 )
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152 use super::*;
1153
1154 #[test]
1155 fn parses_line_oriented_config() -> Result<()> {
1156 let config = Config::parse(
1157 "output = generated.rs\nclass = Object\nmethod = UserGlobals:Order>>findById: | args=id | return=Oop | doc=Find an order.\n",
1158 Some(Path::new("fixtures/gemstone-rs.codegen")),
1159 )?;
1160
1161 assert_eq!(config.output, PathBuf::from("fixtures/generated.rs"));
1162 assert_eq!(config.classes.len(), 2);
1163 assert_eq!(config.classes[0].class_ref.class_name, "Object");
1164 assert_eq!(config.classes[1].class_ref.dictionary, "UserGlobals");
1165 assert_eq!(config.classes[1].methods[0].selector, "findById:");
1166 assert_eq!(config.classes[1].methods[0].args, vec!["id"]);
1167 assert_eq!(config.classes[1].methods[0].return_type, ReturnType::Oop);
1168 Ok(())
1169 }
1170
1171 #[test]
1172 fn parses_class_side_references() {
1173 let class_ref = ClassRef::parse("UserGlobals:Order class").unwrap();
1174 assert_eq!(class_ref.dictionary, "UserGlobals");
1175 assert_eq!(class_ref.class_name, "Order");
1176 assert!(class_ref.meta);
1177 assert_eq!(class_ref.display_name(), "UserGlobals:Order class");
1178 assert_eq!(class_ref.struct_name(), "OrderClass");
1179 }
1180
1181 #[test]
1182 fn sanitizes_selectors_to_rust_function_names() {
1183 assert_eq!(rust_fn_name("printString"), "print_string");
1184 assert_eq!(rust_fn_name("at:put:"), "at_put");
1185 assert_eq!(rust_fn_name("class"), "class");
1186 assert_eq!(rust_fn_name("type"), "type_");
1187 }
1188
1189 #[test]
1190 fn generates_wrapper_source() -> Result<()> {
1191 let config = Config::parse(
1192 "class = Object\nmethod = Object>>printString | return=String | doc=Print the receiver.\nmethod = Object>>at:put: | args=key,value\n",
1193 None,
1194 )?;
1195 let generated = generate(&config);
1196 assert!(generated.source.contains("pub struct Object<'a>"));
1197 assert!(generated.source.contains("/// Print the receiver."));
1198 assert!(generated
1199 .source
1200 .contains("pub fn print_string(&mut self) -> Result<String>"));
1201 assert!(generated
1202 .source
1203 .contains("pub fn at_put(&mut self, key: Oop, value: Oop)"));
1204 assert!(generated
1205 .source
1206 .contains("self.session.perform(self.oop, \"at:put:\", &[key, value])"));
1207 Ok(())
1208 }
1209
1210 #[test]
1211 fn generates_bridge_mapped_struct_source() -> Result<()> {
1212 let config = Config::parse(
1213 "mapped = BookingDraft | doc=Payload stored under BridgeRoot.\nfield = BookingDraft.name | type=String | key=name\nfield = BookingDraft.amount | type=SmallInt | key=amount\nfield = BookingDraft.approved | type=Bool | key=approved\n",
1214 None,
1215 )?;
1216 assert_eq!(config.mapped.len(), 1);
1217 assert_eq!(config.mapped[0].fields.len(), 3);
1218
1219 let generated = generate(&config);
1220 assert!(generated.source.contains("pub struct BookingDraft"));
1221 assert!(generated
1222 .source
1223 .contains("impl BridgeMapped for BookingDraft"));
1224 assert!(generated.source.contains("pub amount: i64"));
1225 assert!(generated
1226 .source
1227 .contains("amount: BridgeFieldRead::read_bridge_field"));
1228 assert!(generated
1229 .source
1230 .contains("BridgeFieldWrite::to_bridge_field_value(&self.approved)"));
1231 Ok(())
1232 }
1233
1234 #[test]
1235 fn parses_symbol_keys_and_nested_field_types() -> Result<()> {
1236 let config = Config::parse(
1237 "mapped = BookingDraft\nfield = BookingDraft.customer | type=Mapped<Customer> | key=customer | key_type=Symbol\nfield = BookingDraft.tags | type=Vec<String> | key=tags\n",
1238 None,
1239 )?;
1240 let fields = &config.mapped[0].fields;
1241 assert_eq!(fields[0].key_type, FieldKeyType::Symbol);
1242 assert_eq!(
1243 fields[0].field_type,
1244 FieldType::Mapped("Customer".to_string())
1245 );
1246 assert_eq!(
1247 fields[1].field_type,
1248 FieldType::Vec(Box::new(FieldType::String))
1249 );
1250 let generated = generate(&config);
1251 assert!(generated.source.contains("BridgeKeyType::Symbol"));
1252 assert!(generated.source.contains("pub tags: Vec<String>"));
1253 Ok(())
1254 }
1255
1256 #[test]
1257 fn creates_config_source_and_diff() -> Result<()> {
1258 let config = Config::parse("class = Object\nmethod = Object>>class\n", None)?;
1259 let source = config_source(&config);
1260 assert!(source.contains("method = Object>>class"));
1261 let report = diff(&Config {
1262 output: std::env::temp_dir().join("gemstone-rs-missing-diff.rs"),
1263 classes: config.classes,
1264 mapped: config.mapped,
1265 })?;
1266 assert!(!report.up_to_date);
1267 assert!(report.diff.contains("+++"));
1268 Ok(())
1269 }
1270
1271 #[test]
1272 fn check_reports_missing_output_as_stale() -> Result<()> {
1273 let nonce = std::time::SystemTime::now()
1274 .duration_since(std::time::UNIX_EPOCH)
1275 .map(|duration| duration.as_nanos())
1276 .unwrap_or_default();
1277 let output = std::env::temp_dir().join(format!("gemstone-rs-codegen-{nonce}.rs"));
1278 let config = Config {
1279 output,
1280 classes: Vec::new(),
1281 mapped: Vec::new(),
1282 };
1283 let report = check(&config)?;
1284 assert!(!report.exists);
1285 assert!(!report.up_to_date);
1286 Ok(())
1287 }
1288}