Skip to main content

cedar_policy_cli/utils/
schema.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use cedar_policy::Schema;
18use clap::{Args, ValueEnum};
19use miette::{Result, WrapErr};
20use std::path::{Path, PathBuf};
21
22use crate::utils::read_from_file;
23
24#[derive(Debug, Default, Clone, Copy, ValueEnum)]
25pub enum SchemaFormat {
26    /// the Cedar format
27    #[default]
28    Cedar,
29    /// JSON format
30    Json,
31}
32
33/// This struct contains the arguments that together specify an input schema.
34#[derive(Args, Debug)]
35pub struct SchemaArgs {
36    /// File containing the schema
37    #[arg(short, long = "schema", value_name = "FILE")]
38    pub schema_file: PathBuf,
39    /// Schema format
40    #[arg(long, value_enum, default_value_t)]
41    pub schema_format: SchemaFormat,
42}
43
44impl SchemaArgs {
45    /// Turn this `SchemaArgs` into the appropriate `Schema` object
46    pub(crate) fn get_schema(&self) -> Result<Schema> {
47        read_schema_from_file(&self.schema_file, self.schema_format)
48    }
49}
50
51/// This struct contains the arguments that together specify an input schema,
52/// for commands where the schema is optional.
53#[derive(Args, Debug)]
54pub struct OptionalSchemaArgs {
55    /// File containing the schema
56    #[arg(short, long = "schema", value_name = "FILE")]
57    pub schema_file: Option<PathBuf>,
58    /// Schema format
59    #[arg(long, value_enum, default_value_t)]
60    pub schema_format: SchemaFormat,
61}
62
63impl OptionalSchemaArgs {
64    /// Turn this `OptionalSchemaArgs` into the appropriate `Schema` object, or `None`
65    pub(crate) fn get_schema(&self) -> Result<Option<Schema>> {
66        let Some(schema_file) = &self.schema_file else {
67            return Ok(None);
68        };
69        read_schema_from_file(schema_file, self.schema_format).map(Some)
70    }
71}
72
73fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
74    let path = path.as_ref();
75    let schema_src = read_from_file(path, "schema")?;
76    match format {
77        SchemaFormat::Json => Schema::from_json_str(&schema_src)
78            .wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
79        SchemaFormat::Cedar => {
80            let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
81                .wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
82            for warning in warnings {
83                let report = miette::Report::new(warning);
84                eprintln!("{report:?}");
85            }
86            Ok(schema)
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::utils::test_utils::{render_err, TEMPFILE_FILTER};
95    use std::io::Write;
96
97    #[test]
98    fn cedar_schema_from_file_parse_error() {
99        let mut f = tempfile::NamedTempFile::new().unwrap();
100        f.write_all(b"not a valid schema").unwrap();
101        let err = read_schema_from_file(f.path(), SchemaFormat::Cedar).unwrap_err();
102        insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
103            insta::assert_snapshot!(render_err(&err), @r"
104             × failed to parse schema from file <TEMPFILE>
105             ╰─▶ error parsing schema: unexpected token `not`
106              ╭────
107            1 │ not a valid schema
108              · ─┬─
109              ·  ╰── expected `@`, `action`, `entity`, `namespace`, or `type`
110              ╰────
111            ");
112        });
113    }
114
115    #[test]
116    fn json_schema_from_file_parse_error() {
117        let mut f = tempfile::NamedTempFile::new().unwrap();
118        f.write_all(b"not json").unwrap();
119        let err = read_schema_from_file(f.path(), SchemaFormat::Json).unwrap_err();
120        insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
121            insta::assert_snapshot!(render_err(&err), @r"
122            × failed to parse schema from file <TEMPFILE>
123            ╰─▶ expected ident at line 1 column 2
124            help: this API was expecting a schema in the JSON format; did you mean to use a different function, which expects the Cedar schema format?
125            ");
126        });
127    }
128
129    #[test]
130    fn schema_from_missing_file() {
131        let err = read_schema_from_file("/tmp/nonexistent_cedar_schema.json", SchemaFormat::Cedar)
132            .unwrap_err();
133        insta::assert_snapshot!(render_err(&err), @r"
134        × failed to open schema file /tmp/nonexistent_cedar_schema.json
135        ╰─▶ No such file or directory (os error 2)
136        ");
137    }
138}