Skip to main content

gemstone_rs/
codegen.rs

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, &current, &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}