Skip to main content

xidl_parser/hir/
spec.rs

1use super::{
2    Definition, InterfaceDcl, ModuleDcl, ParserProperties, Specification, TypeDcl,
3    expand_annotations, include, interface_codegen, parse_xidlc_pragma,
4};
5use crate::jsonrpc_hir;
6use crate::rest_hir::{self, HirProjectionKind, ProjectedHir};
7use crate::semantic;
8use serde_json::Value;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12impl From<crate::typed_ast::Specification> for Specification {
13    fn from(value: crate::typed_ast::Specification) -> Self {
14        spec_from_typed_ast(value, true)
15    }
16}
17
18impl Specification {
19    pub fn from_typed_ast_with_properties(
20        value: crate::typed_ast::Specification,
21        properties: ParserProperties,
22    ) -> Self {
23        spec_from_typed_ast(value, expand_interface(&properties))
24    }
25
26    pub fn from_typed_ast_with_properties_and_path(
27        value: crate::typed_ast::Specification,
28        properties: ParserProperties,
29        path: impl AsRef<Path>,
30    ) -> crate::error::ParserResult<Self> {
31        spec_from_typed_ast_with_path(value, expand_interface(&properties), path.as_ref())
32    }
33
34    pub fn from_typed_ast_with_path(
35        value: crate::typed_ast::Specification,
36        path: impl AsRef<Path>,
37    ) -> crate::error::ParserResult<Self> {
38        spec_from_typed_ast_with_path(value, true, path.as_ref())
39    }
40
41    pub fn project_typed_ast_with_properties_and_path(
42        value: crate::typed_ast::Specification,
43        properties: ParserProperties,
44        path: impl AsRef<Path>,
45    ) -> crate::error::ParserResult<ProjectedHir> {
46        let spec =
47            spec_from_typed_ast_with_path(value, expand_interface(&properties), path.as_ref())?;
48        match hir_projection_kind(&properties) {
49            HirProjectionKind::Rpc => Ok(ProjectedHir::Rpc(spec)),
50            HirProjectionKind::Http => rest_hir::project(&spec).map(ProjectedHir::Http),
51            HirProjectionKind::JsonRpc => jsonrpc_hir::project(&spec).map(ProjectedHir::JsonRpc),
52        }
53    }
54}
55
56pub(crate) fn spec_from_typed_ast(
57    value: crate::typed_ast::Specification,
58    expand_interfaces: bool,
59) -> Specification {
60    let mut definitions = Vec::new();
61    collect_defs_with_context(
62        value.0,
63        &mut Vec::new(),
64        expand_interfaces,
65        &mut definitions,
66        None,
67        None,
68    )
69    .expect("pathless HIR conversion should not fail");
70    let mut spec = Specification(definitions);
71    semantic::analyze(&mut spec);
72    spec
73}
74
75fn spec_from_typed_ast_with_path(
76    value: crate::typed_ast::Specification,
77    expand_interfaces: bool,
78    path: &Path,
79) -> crate::error::ParserResult<Specification> {
80    let root = include::normalize_path(path);
81    let mut definitions = Vec::new();
82    let mut include_stack = vec![root.clone()];
83    collect_defs_with_context(
84        value.0,
85        &mut Vec::new(),
86        expand_interfaces,
87        &mut definitions,
88        Some(root.as_path()),
89        Some(&mut include_stack),
90    )?;
91    let mut spec = Specification(definitions);
92    semantic::analyze(&mut spec);
93    Ok(spec)
94}
95
96fn collect_defs_with_context(
97    defs: Vec<crate::typed_ast::Definition>,
98    modules: &mut Vec<String>,
99    expand_interfaces: bool,
100    out: &mut Vec<Definition>,
101    current_file: Option<&Path>,
102    mut include_stack: Option<&mut Vec<PathBuf>>,
103) -> crate::error::ParserResult<()> {
104    for def in defs {
105        match def {
106            crate::typed_ast::Definition::ModuleDcl(module) => {
107                let ident = module.ident.0;
108                let annotations = expand_annotations(module.annotations);
109                modules.push(ident.clone());
110                let mut inner = Vec::new();
111                collect_defs_with_context(
112                    module.definition,
113                    modules,
114                    expand_interfaces,
115                    &mut inner,
116                    current_file,
117                    include_stack.as_deref_mut(),
118                )?;
119                modules.pop();
120                out.push(Definition::ModuleDcl(ModuleDcl {
121                    annotations,
122                    ident,
123                    definition: inner,
124                }));
125            }
126            crate::typed_ast::Definition::PreprocCall(call) => {
127                if let Some(pragma) = parse_xidlc_pragma(&call) {
128                    out.push(Definition::Pragma(pragma));
129                }
130            }
131            crate::typed_ast::Definition::TypeDcl(value) => {
132                out.push(Definition::TypeDcl(TypeDcl::from(value)))
133            }
134            crate::typed_ast::Definition::ConstDcl(value) => {
135                out.push(Definition::ConstDcl(value.into()))
136            }
137            crate::typed_ast::Definition::ExceptDcl(value) => {
138                out.push(Definition::ExceptDcl(value.into()))
139            }
140            crate::typed_ast::Definition::InterfaceDcl(value) => {
141                let interface = InterfaceDcl::from(value);
142                if expand_interfaces {
143                    let generated = interface_codegen::expand_interface(&interface, modules)
144                        .unwrap_or_else(|err| panic!("interface expansion failed: {err}"));
145                    out.extend(generated);
146                }
147                out.push(Definition::InterfaceDcl(interface));
148            }
149            crate::typed_ast::Definition::PreprocInclude(include_def) => {
150                let Some(current_file) = current_file else {
151                    continue;
152                };
153                let path = include::resolve_include_path(current_file, &include_def)?;
154                let typed = parse_included_specification(&path)?;
155                let stack = include_stack
156                    .as_deref_mut()
157                    .expect("include stack must exist when current file path is set");
158                guard_include_cycle(stack, &path)?;
159                stack.push(path.clone());
160                collect_defs_with_context(
161                    typed.0,
162                    modules,
163                    expand_interfaces,
164                    out,
165                    Some(path.as_path()),
166                    Some(stack),
167                )?;
168                stack.pop();
169            }
170            crate::typed_ast::Definition::TemplateModuleDcl(_)
171            | crate::typed_ast::Definition::TemplateModuleInst(_)
172            | crate::typed_ast::Definition::PreprocDefine(_) => {}
173        }
174    }
175
176    Ok(())
177}
178
179fn expand_interface(properties: &ParserProperties) -> bool {
180    properties
181        .get("expand_interface")
182        .and_then(Value::as_bool)
183        .unwrap_or(true)
184}
185
186fn hir_projection_kind(properties: &ParserProperties) -> HirProjectionKind {
187    match properties.get("hir_kind").and_then(Value::as_str) {
188        Some(value) if value.eq_ignore_ascii_case("http") => HirProjectionKind::Http,
189        Some(value)
190            if value.eq_ignore_ascii_case("jsonrpc") || value.eq_ignore_ascii_case("json-rpc") =>
191        {
192            HirProjectionKind::JsonRpc
193        }
194        _ => HirProjectionKind::Rpc,
195    }
196}
197
198fn parse_included_specification(
199    path: &Path,
200) -> crate::error::ParserResult<crate::typed_ast::Specification> {
201    let source = fs::read_to_string(path).map_err(|err| {
202        crate::error::ParseError::Message(format!(
203            "failed to read include '{}': {err}",
204            path.display()
205        ))
206    })?;
207    crate::parser::parser_text(&source).map_err(|err| {
208        crate::error::ParseError::Message(format!(
209            "failed to parse include '{}': {err}",
210            path.display()
211        ))
212    })
213}
214
215fn guard_include_cycle(stack: &[PathBuf], path: &Path) -> crate::error::ParserResult<()> {
216    if stack.contains(&path.to_path_buf()) {
217        let chain = stack
218            .iter()
219            .chain(std::iter::once(&path.to_path_buf()))
220            .map(|path| path.display().to_string())
221            .collect::<Vec<_>>()
222            .join(" -> ");
223        return Err(crate::error::ParseError::Message(format!(
224            "cyclic include detected: {chain}"
225        )));
226    }
227    Ok(())
228}