Repository: cedar-policy/cedar (rust)

Issue:
[CLI] `check-parse` command (or another command) should check parsing of entity data and schemas, not just policies
### Category

Other

### Describe the feature you'd like to request

Currently, the CLI `check-parse` command only checks the parsing of policies.

To check the parsing of entity data or schemas, you have to use other commands -- for instance `authorize` for entity data, or `validate` for schemas.  But this is inconvenient, as you also have to provide other inputs for those commands, like policies.  We don't have a dedicated way to check the parsing of entity data or schemas without also having to provide unnecessary inputs.

### Describe alternatives you've considered

We could have separate commands `check-policies-parse`, `check-entities-parse`, `check-schema-parse`; or (my preference) just have a single `check-parse` which checks that any or all of these parse.  `check-parse` could check the parsing of any or all of these in a single call -- e.g., if you pass just schema, it checks the parsing of the schema, or if you pass both entities and policies, it checks the parsing of the entities and policies; etc.

### Additional context

_No response_

### Is this something that you'd be interested in working on?

- [ ] 👋 I may be able to implement this feature request
- [ ] ⚠️ This feature might incur a breaking change

Pull request description:
[CLI] improve check-parse command
## Description of changes

Makes the improvements to CLI `check-parse` that were suggested in #415.  Specifically, allows the `check-parse` command to check the parse of policies, schema, and/or entities, whatever the user chooses to pass to the command.

For backwards compatibility, preserves the behavior that if no policies, schema, or entities are passed, `check-parse` will read policies from stdin and check that they parse.

## Issue #, if available

Fixes #415

## Checklist for requesting a review

The change in this PR is (choose one, and delete the other options):

- [x] A backwards-compatible change requiring a minor version bump to `cedar-policy` (e.g., addition of a new API).

I confirm that this PR (choose one, and delete the other options):

- [x] Updates the "Unreleased" section of the CHANGELOG with a description of my change (required for major/minor version bumps).

I confirm that [`cedar-spec`](https://github.com/cedar-policy/cedar-spec) (choose one, and delete the other options):

- [x] Does not require updates because my change does not impact the Cedar formal model or DRT infrastructure.

I confirm that [`docs.cedarpolicy.com`](https://docs.cedarpolicy.com/) (choose one, and delete the other options):

- [x] Does not require updates because my change does not impact the Cedar language specification.


Patch:
diff --git a/cedar-policy-cli/CHANGELOG.md b/cedar-policy-cli/CHANGELOG.md
index 90c48b04..fa17cd34 100644
--- a/cedar-policy-cli/CHANGELOG.md
+++ b/cedar-policy-cli/CHANGELOG.md
@@ -8,8 +8,10 @@ Changes to the Cedar language, which are likely to affect users of the CLI, are
 ### Added
 
 - Added `json-to-cedar` direction to `translate-policy` command. (#1510, resolving #461)
-- Add `--level` option to the `validate` command, exposing the `level-validate`
+- Added `--level` option to the `validate` command, exposing the `level-validate`
   experimental feature through the CLI. (#1508, resolving #1501)
+- Improved the `check-parse` command, which now checks the parse of policies, schema,
+  and/or entities (whatever is passed). (#1548)
 
 ## 4.3.3
 
diff --git a/cedar-policy-cli/src/lib.rs b/cedar-policy-cli/src/lib.rs
index a05acf0c..8f37da8a 100644
--- a/cedar-policy-cli/src/lib.rs
+++ b/cedar-policy-cli/src/lib.rs
@@ -89,7 +89,10 @@ pub enum Commands {
     Evaluate(EvaluateArgs),
     /// Validate a policy set against a schema
     Validate(ValidateArgs),
-    /// Check that policies successfully parse
+    /// Check that policies, schema, and/or entities successfully parse.
+    /// (All arguments are optional; this checks that whatever is provided parses)
+    ///
+    /// If no arguments are provided, reads policies from stdin and checks that they parse.
     CheckParse(CheckParseArgs),
     /// Link a template
     Link(LinkArgs),
@@ -197,7 +200,13 @@ pub struct ValidateArgs {
 pub struct CheckParseArgs {
     /// Policies args (incorporated by reference)
     #[command(flatten)]
-    pub policies: PoliciesArgs,
+    pub policies: OptionalPoliciesArgs,
+    /// Schema args (incorporated by reference)
+    #[command(flatten)]
+    pub schema: OptionalSchemaArgs,
+    /// File containing JSON representation of a Cedar entity hierarchy
+    #[arg(long = "entities", value_name = "FILE")]
+    pub entities_file: Option<PathBuf>,
 }
 
 /// This struct contains the arguments that together specify a request.
@@ -477,6 +486,40 @@ impl PoliciesArgs {
     }
 }
 
+/// This struct contains the arguments that together specify an input policy or policy set,
+/// for commands where policies are optional.
+#[derive(Args, Debug)]
+pub struct OptionalPoliciesArgs {
+    /// File containing static Cedar policies and/or templates
+    #[arg(short, long = "policies", value_name = "FILE")]
+    pub policies_file: Option<String>,
+    /// Format of policies in the `--policies` file
+    #[arg(long = "policy-format", default_value_t, value_enum)]
+    pub policy_format: PolicyFormat,
+    /// File containing template-linked policies. Ignored if `--policies` is not
+    /// present (because in that case there are no templates to link against)
+    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
+    pub template_linked_file: Option<String>,
+}
+
+impl OptionalPoliciesArgs {
+    /// Turn this `OptionalPoliciesArgs` into the appropriate `PolicySet`
+    /// object, or `None` if no policies were provided
+    fn get_policy_set(&self) -> Result<Option<PolicySet>> {
+        match &self.policies_file {
+            None => Ok(None),
+            Some(policies_file) => {
+                let pargs = PoliciesArgs {
+                    policies_file: Some(policies_file.clone()),
+                    policy_format: self.policy_format,
+                    template_linked_file: self.template_linked_file.clone(),
+                };
+                pargs.get_policy_set().map(Some)
+            }
+        }
+    }
+}
+
 /// This struct contains the arguments that together specify an input schema.
 #[derive(Args, Debug)]
 pub struct SchemaArgs {
@@ -759,13 +802,55 @@ impl Termination for CedarExitCode {
 }
 
 pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
+    // for backwards compatibility: if no policies/schema/entities are provided,
+    // read policies from stdin and check that they parse
+    if (
+        &args.policies.policies_file,
+        &args.schema.schema_file,
+        &args.entities_file,
+    ) == (&None, &None, &None)
+    {
+        let pargs = PoliciesArgs {
+            policies_file: None, // read from stdin
+            policy_format: args.policies.policy_format,
+            template_linked_file: args.policies.template_linked_file.clone(),
+        };
+        match pargs.get_policy_set() {
+            Ok(_) => return CedarExitCode::Success,
+            Err(e) => {
+                println!("{e:?}");
+                return CedarExitCode::Failure;
+            }
+        }
+    }
+
+    let mut exit_code = CedarExitCode::Success;
     match args.policies.get_policy_set() {
-        Ok(_) => CedarExitCode::Success,
+        Ok(_) => (),
         Err(e) => {
             println!("{e:?}");
-            CedarExitCode::Failure
+            exit_code = CedarExitCode::Failure;
+        }
+    }
+    let schema = match args.schema.get_schema() {
+        Ok(schema) => schema,
+        Err(e) => {
+            println!("{e:?}");
+            exit_code = CedarExitCode::Failure;
+            None
         }
+    };
+    match &args.entities_file {
+        None => (),
+        Some(efile) => match load_entities(efile, schema.as_ref()) {
+            Ok(_) => (),
+            Err(e) => {
+                println!("{e:?}");
+                exit_code = CedarExitCode::Failure;
+            }
+        },
     }
+    exit_code
 }
 
 pub fn validate(args: &ValidateArgs) -> CedarExitCode {


Test patch:
diff --git a/cedar-policy-cli/tests/sample.rs b/cedar-policy-cli/tests/sample.rs
index 690fe23a..c7c4b455 100644
--- a/cedar-policy-cli/tests/sample.rs
+++ b/cedar-policy-cli/tests/sample.rs
@@ -23,30 +23,39 @@ use std::path::{Path, PathBuf};
 
 use cedar_policy::EvalResult;
 use cedar_policy::SlotId;
-use cedar_policy_cli::check_parse;
-use cedar_policy_cli::OptionalSchemaArgs;
-use cedar_policy_cli::SchemaArgs;
-use cedar_policy_cli::SchemaFormat;
 use cedar_policy_cli::{
-    authorize, evaluate, link, validate, Arguments, AuthorizeArgs, CedarExitCode, CheckParseArgs,
-    EvaluateArgs, LinkArgs, PoliciesArgs, PolicyFormat, RequestArgs, ValidateArgs,
+    authorize, check_parse, evaluate, link, validate, Arguments, AuthorizeArgs, CedarExitCode,
+    CheckParseArgs, EvaluateArgs, LinkArgs, OptionalPoliciesArgs, OptionalSchemaArgs, PoliciesArgs,
+    PolicyFormat, RequestArgs, SchemaArgs, SchemaFormat, ValidateArgs,
 };
 
 use predicates::prelude::*;
 use rstest::rstest;
 
-fn run_check_parse_test(policies_file: impl Into<String>, expected_exit_code: CedarExitCode) {
+#[track_caller]
+fn run_check_parse_test(
+    policies_file: impl Into<String>,
+    schema_file: impl Into<PathBuf>,
+    entities_file: Option<impl Into<PathBuf>>,
+    expected_exit_code: CedarExitCode,
+) {
     let cmd = CheckParseArgs {
-        policies: PoliciesArgs {
+        policies: OptionalPoliciesArgs {
             policies_file: Some(policies_file.into()),
             policy_format: PolicyFormat::Cedar,
             template_linked_file: None,
         },
+        schema: OptionalSchemaArgs {
+            schema_file: Some(schema_file.into()),
+            schema_format: SchemaFormat::Cedar,
+        },
+        entities_file: entities_file.map(Into::into),
     };
     let output = check_parse(&cmd);
     assert_eq!(output, expected_exit_code, "{:#?}", cmd);
 }
 
+#[track_caller]
 fn run_authorize_test(
     policies_file: impl Into<String>,
     entities_file: impl Into<String>,
@@ -66,6 +75,7 @@ fn run_authorize_test(
     );
 }
 
+#[track_caller]
 fn run_authorize_test_with_linked_policies(
     policies_file: impl Into<String>,
     entities_file: impl Into<String>,
@@ -101,6 +111,7 @@ fn run_authorize_test_with_linked_policies(
     assert_eq!(exit_code, output, "{:#?}", cmd,);
 }
 
+#[track_caller]
 fn run_link_test(
     policies_file: impl Into<String>,
     links_file: impl Into<String>,
@@ -125,6 +136,7 @@ fn run_link_test(
     assert_eq!(output, expected);
 }
 
+#[track_caller]
 fn run_authorize_test_context(
     policies_file: impl Into<String>,
     entities_file: impl Into<String>,
@@ -160,6 +172,7 @@ fn run_authorize_test_context(
     assert_eq!(exit_code, output, "{:#?}", cmd,);
 }
 
+#[track_caller]
 fn run_authorize_test_json(
     policies_file: impl Into<String>,
     entities_file: impl Into<String>,
@@ -196,6 +209,8 @@ fn run_authorize_test_json(
 fn test_authorize_samples() {
     run_check_parse_test(
         "sample-data/sandbox_a/policies_1.cedar",
+        "sample-data/sandbox_a/schema.cedarschema",
+        None::<PathBuf>,
         CedarExitCode::Success,
     );
     run_authorize_test(
@@ -208,6 +223,8 @@ fn test_authorize_samples() {
     );
     run_check_parse_test(
         "sample-data/sandbox_a/policies_2.cedar",
+        "sample-data/sandbox_a/schema.cedarschema",
+        None::<PathBuf>,
         CedarExitCode::Success,
     );
     run_authorize_test(
@@ -276,6 +293,8 @@ fn test_authorize_samples() {
     );
     run_check_parse_test(
         "sample-data/sandbox_a/policies_3.cedar",
+        "sample-data/sandbox_a/schema.cedarschema",
+        None::<PathBuf>,
         CedarExitCode::Success,
     );
     run_authorize_test(
@@ -329,6 +348,8 @@ fn test_authorize_samples() {
 
     run_check_parse_test(
         "sample-data/sandbox_b/policies_4.cedar",
+        "sample-data/sandbox_b/schema.cedarschema",
+        None::<PathBuf>,
         CedarExitCode::Success,
     );
     run_authorize_test(
@@ -365,6 +386,8 @@ fn test_authorize_samples() {
     );
     run_check_parse_test(
         "sample-data/sandbox_b/policies_5.cedar",
+        "sample-data/sandbox_b/schema.cedarschema",
+        None::<PathBuf>,
         CedarExitCode::Success,
     );
     run_authorize_test(