diff --git a/src/diagnostics/src/diagnostic.rs b/src/diagnostics/src/diagnostic.rs index d296fee..0319507 100644 --- a/src/diagnostics/src/diagnostic.rs +++ b/src/diagnostics/src/diagnostic.rs @@ -21,11 +21,13 @@ where } } +/// The template resource a diagnostic is attributed to, when it targets one. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ResourceRef { + /// Logical ID of the resource as declared in the template. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub id: Option, @@ -34,33 +36,41 @@ pub struct ResourceRef { pub resource_type: Option, } +/// Extra detail about a specific violation, present only in the detailed report. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ViolationContext { + /// The resolved property value that triggered the violation. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "wasm-bindings", tsify(type = "JsonValue"))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub actual_value: Option, + /// The constraint the value was expected to satisfy (such as the required type or allowed pattern). #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub expected_constraint: Option, + /// Name of the offending property. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub property: Option, + /// Lifecycle marker for the flagged resource type or property, such as 'deprecated', 'create-only', or 'write-only'. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub lifecycle: Option, + /// How the offending value was derived, such as a Ref, Fn::GetAtt, Fn::If, or parameter. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub resolution_source: Option, + /// Additional finding-specific values keyed by name. #[serde(default, skip_serializing_if = "Option::is_none", serialize_with = "serialize_sorted_optional_map")] #[cfg_attr(feature = "wasm-bindings", tsify(type = "Record"))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub extra: Option>, } +/// Another resource involved in the diagnostic, such as the target of a reference. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -69,6 +79,7 @@ pub struct RelatedResource { #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub resource: Option, + /// Source location of the related resource. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub location: Option, @@ -127,22 +138,27 @@ impl Filterable for Diagnostic { /// `resource_id`/`resource_type` and `location` into individual line/column /// fields. Used by `StandardDiagnostic` and `DetailedDiagnostic`. macro_rules! define_flattened_diagnostic { - ($name:ident $(, $extra_field:ident : $extra_ty:ty)*) => { + ($(#[$struct_meta:meta])* $name:ident $(, $(#[$extra_meta:meta])* $extra_field:ident : $extra_ty:ty)*) => { + $(#[$struct_meta])* #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct $name { + /// Identifier of the rule that produced this finding; its leading letter encodes the severity. pub rule_id: String, pub severity: Severity, pub message: String, + /// Where the rule came from, such as a provider schema, the built-in engine, or a user-supplied rule. pub source: RuleOrigin, + /// Logical ID of the resource this finding targets, if any. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub resource_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub resource_type: Option, + /// Path to the offending property within the resource, such as 'Properties.Name'. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub property_path: Option, @@ -152,6 +168,7 @@ macro_rules! define_flattened_diagnostic { #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub category: Option, + /// Line in the source template where the finding begins (1-based). #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub start_line: Option, @@ -167,11 +184,13 @@ macro_rules! define_flattened_diagnostic { #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub related_resources: Option>, + /// Condition name to boolean assignment under which this finding applies, when it depends on template conditions. #[serde(default, skip_serializing_if = "Option::is_none", serialize_with = "serialize_sorted_optional_map")] #[cfg_attr(feature = "wasm-bindings", tsify(type = "Record"))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub condition_scenario: Option>, $( + $(#[$extra_meta])* #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub $extra_field: $extra_ty, @@ -180,11 +199,17 @@ macro_rules! define_flattened_diagnostic { }; } -define_flattened_diagnostic!(StandardDiagnostic); -define_flattened_diagnostic!(DetailedDiagnostic, +define_flattened_diagnostic!( + /// A single validation finding with its resource and source location flattened into individual fields. + StandardDiagnostic +); +define_flattened_diagnostic!( + /// A validation finding with additional context and enrichment beyond the standard finding. + DetailedDiagnostic, documentation_url: Option, rule_description: Option, phase: Option, + /// Top-level template section the finding falls under, such as 'Resources' or 'Parameters'. section: Option, context: Option ); @@ -305,16 +330,21 @@ impl Diagnostic { } } +/// Timing breakdown of the validation run, per pipeline phase. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct PerformanceMetrics { + /// Time to load the provider schemas. pub schema_init: PhaseMetric, + /// Time to initialize the rule evaluation engine. pub engine_init: PhaseMetric, + /// Time to parse the template and build its model. pub model_build: PhaseMetric, pub schema_validate: PhaseMetric, pub rule_evaluation: PhaseMetric, + /// Time spent enriching, filtering, sorting, and finalizing the diagnostics after rule evaluation. pub diagnostic_finalize: PhaseMetric, pub validate_total: PhaseMetric, } @@ -324,13 +354,18 @@ pub struct PerformanceMetrics { #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ReportMetadata { + /// Number of rules that were active for this run after any category exclusions. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub rules_evaluated: Option, pub resources_scanned: u32, + /// Tally of reported diagnostics by severity. pub counts: Summary, + /// Number of diagnostics removed by filters and the severity threshold. pub suppressed: u32, + /// Whether strict mode was enabled, promoting warnings to errors. pub strict: bool, + /// Minimum severity included in the report; lower-severity findings are omitted. pub severity_level: Severity, } @@ -393,6 +428,7 @@ impl ValidationReport { } } +/// Standard validation result: the report plus flattened diagnostics. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -406,6 +442,7 @@ pub struct StandardReport { pub diagnostics: Vec, } +/// Detailed validation result: like the standard report but with per-diagnostic context and enrichment. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] diff --git a/src/diagnostics/src/json_value.rs b/src/diagnostics/src/json_value.rs index 3f4ac3f..18b33f6 100644 --- a/src/diagnostics/src/json_value.rs +++ b/src/diagnostics/src/json_value.rs @@ -1,10 +1,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::ops::Deref; -/// Newtype around `serde_json::Value` that provides WASM (via `tsify`) and -/// UniFFI bindings. WASM serializes through serde into native JS values. -/// UniFFI bridges through a recursive `JsonValueEnum` so consumers get -/// typed data without parsing JSON strings. +/// An arbitrary JSON value (null, boolean, number, string, array, or object). #[derive(Debug, Clone, PartialEq)] pub struct JsonValue(pub serde_json::Value); diff --git a/src/diagnostics/src/metrics.rs b/src/diagnostics/src/metrics.rs index 9d90ba4..c689f76 100644 --- a/src/diagnostics/src/metrics.rs +++ b/src/diagnostics/src/metrics.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use web_time::Instant; +/// Timing for a single phase of validation (parsing, schema validation, rule evaluation, etc.). #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] diff --git a/src/diagnostics/src/span.rs b/src/diagnostics/src/span.rs index e44da3f..80f039d 100644 --- a/src/diagnostics/src/span.rs +++ b/src/diagnostics/src/span.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +/// A range in the source template. Lines and columns are 1-based. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] diff --git a/src/rules/src/filter.rs b/src/rules/src/filter.rs index c7b6045..8417fff 100644 --- a/src/rules/src/filter.rs +++ b/src/rules/src/filter.rs @@ -68,8 +68,7 @@ pub struct ServiceFilter { pub service: String, } -/// Filter criteria across seven dimensions: rule IDs, categories, ID ranges, regex -/// patterns, resource IDs, resource types, and services. +/// A set of rule-matching criteria; a rule matches when it satisfies any one of them. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm-bindings", tsify(from_wasm_abi))] @@ -85,6 +84,7 @@ pub struct RuleFilterConfig { #[serde(default)] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub id_ranges: Vec, + /// Regular expressions matched against the rule ID. #[serde(default)] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub id_patterns: Vec, diff --git a/src/rules/src/registry.rs b/src/rules/src/registry.rs index 2853822..2010af9 100644 --- a/src/rules/src/registry.rs +++ b/src/rules/src/registry.rs @@ -14,7 +14,7 @@ pub struct RuleDefinition { pub origin: RuleOrigin, } -/// Serializable, owned representation of a rule returned by public APIs. +/// Metadata describing a validation rule. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -37,7 +37,7 @@ pub struct RuleInfo { pub enum RuleOrigin { /// Derived from the CloudFormation resource provider schema Schema, - /// Ported from the cfn-lint + /// Ported from cfn-lint CfnLint, /// Implemented in this validation engine Engine, diff --git a/src/template-model/src/diagnostic.rs b/src/template-model/src/diagnostic.rs index e3f3851..a86ad0f 100644 --- a/src/template-model/src/diagnostic.rs +++ b/src/template-model/src/diagnostic.rs @@ -2,6 +2,8 @@ use diagnostics::JsonValue; use serde::Serialize; use std::collections::HashMap; +/// Advanced introspection view of a parsed CloudFormation template: its reference graph, +/// conditions, and resolved resources and outputs. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -12,10 +14,15 @@ pub struct DiagnosticModel { pub parameters: HashMap, #[cfg_attr(feature = "wasm-bindings", tsify(type = "Record"))] pub conditions: HashMap, + /// Parameter logical IDs referenced by the template's condition expressions. pub condition_param_refs: Vec, + /// Logical implications between conditions: if the antecedent condition holds, the consequent condition must also hold. pub condition_implications: Vec, + /// Groups of conditions that are mutually exclusive because they test the same parameter against different values. pub condition_mutex_groups: Vec, + /// Pairs of condition names that can never both be true at the same time. pub condition_exclusions: Vec>, + /// Maps each resource's logical ID to the name of the condition that gates whether it is created. #[cfg_attr(feature = "wasm-bindings", tsify(type = "Record"))] pub resource_condition_map: HashMap, pub mappings: JsonValue, @@ -24,20 +31,33 @@ pub struct DiagnosticModel { #[cfg_attr(feature = "wasm-bindings", tsify(type = "Record"))] pub outputs: HashMap, pub edges: Vec, + /// Reference cycles in the template, each listed as the resource logical IDs forming the cycle. pub cycles: Vec>, + /// Output names whose value is an Fn::Join with an empty delimiter. pub output_empty_joins: Vec, + /// Logical IDs of resources the SAM transform generates implicitly (such as function execution roles and an implicit REST API). pub sam_implicit_resources: Vec, + /// Parameter logical IDs referenced from the SAM Globals section. pub globals_param_refs: Vec, + /// True when the template appears to be generated by the AWS CDK. pub is_cdk: bool, + /// Names of conditions referenced by Fn::If expressions in the template. pub fn_if_conditions: Vec, + /// Mapping names referenced by Fn::FindInMap expressions. pub find_in_map_names: Vec, + /// Parameter logical IDs referenced from within other parameters' own definitions. pub params_referenced_in_definitions: Vec, + /// True when a Fn::FindInMap uses a computed mapping name rather than a literal string. pub has_dynamic_findinmap_name: bool, + /// True when the template could not be fully parsed and the model is therefore incomplete. pub has_parse_errors: bool, + /// The template's Rules section, each with its optional condition and assertions. pub parsed_rules: Vec, + /// For resolved resource properties, records where each value came from (such as a parameter default, allowed values, an override, or an intrinsic). pub resolution_sources: Vec, } +/// Top-level template metadata. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -50,25 +70,31 @@ pub struct DiagnosticTemplate { #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub description: Option, pub transforms: Vec, + /// The top-level section names present in the template as written. pub raw_top_level_keys: Vec, } +/// A single Conditions entry with its expression and relationships to other conditions. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct DiagnosticCondition { + /// The condition expression rendered as a readable string. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub expression: Option, + /// Names of other conditions this condition depends on. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub deps: Option>, + /// Names of conditions that are mutually exclusive with this one. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub mutex_with: Option>, } +/// An implication between two conditions: whenever the antecedent holds, the consequent also holds. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -78,68 +104,91 @@ pub struct DiagnosticImplication { pub consequent: String, } +/// A set of conditions that are mutually exclusive because they test the same parameter against different values. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct DiagnosticMutexGroup { pub conditions: Vec, + /// The parameter all conditions in the group test. pub parameter: String, + /// The distinct parameter values the conditions compare against, one per condition. pub values: Vec, } +/// A single edge in the template's reference graph, from a referencing resource to its target. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ReferenceEdge { + /// Logical ID of the resource that makes the reference. pub source: String, + /// Property path within the source where the reference occurs. pub source_path: String, + /// Logical ID of the referenced resource or parameter. pub target: String, + /// How the reference is made, such as Ref, GetAtt, Sub, or DependsOn. pub kind: String, + /// For a GetAtt or Sub reference, the attribute or variable name being referenced. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub attr: Option, + /// Name of the condition under which this reference is active, if it is inside a conditional branch. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub condition_context: Option, } +/// A reference a resource makes to another resource or parameter. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct OutgoingRef { + /// Property path within the resource where the reference occurs. pub source_path: String, + /// Logical ID of the referenced resource or parameter. pub target: String, + /// How the reference is made, such as Ref, GetAtt, Sub, or DependsOn. pub kind: String, + /// For a GetAtt or Sub reference, the attribute or variable name being referenced. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub attr: Option, + /// Name of the condition under which this reference is active, if it is inside a conditional branch. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub condition_context: Option, } +/// A reference made to this resource by another resource. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct IncomingRef { + /// Logical ID of the resource that makes the reference. pub source: String, + /// Property path within the source where the reference occurs. pub source_path: String, + /// How the reference is made, such as Ref, GetAtt, Sub, or DependsOn. pub kind: String, + /// For a GetAtt or Sub reference, the attribute or variable name being referenced. #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub attr: Option, } +/// A resource after intrinsic resolution, with its properties, references, and detected findings. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct DiagnosticResource { pub resource_type: String, + /// Name of the condition gating whether this resource is created, if any. #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub condition: Option, @@ -156,22 +205,34 @@ pub struct DiagnosticResource { #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub update_policy: Option, + /// The resource's properties with intrinsics resolved where possible. #[cfg_attr(feature = "wasm-bindings", tsify(type = "Record"))] pub properties: HashMap, pub outgoing_refs: Vec, pub incoming_refs: Vec, + /// Mapping names referenced by Fn::FindInMap within this resource. pub find_in_map_refs: Vec, + /// Fn::Sub uses whose template is a single variable with no surrounding text, which could be written as a plain reference. pub simple_subs: Vec, + /// Property paths where Fn::Sub has no variables to substitute. pub redundant_subs: Vec, + /// Property paths where Fn::Join uses an empty delimiter, concatenating its elements directly. pub empty_joins: Vec, + /// Property paths where an ARN hardcodes the aws partition instead of using AWS::Partition. pub hardcoded_partition_arns: Vec, + /// Properties that resolve to null in one branch of a condition. pub conditionally_null_props: Vec, + /// Names of conditions referenced within this resource's properties. pub condition_refs: Vec, + /// Fn::ForEach loops in this resource, each with its iteration variable and collection source. pub for_each_expansions: Vec, + /// Property paths containing ${...} text that is not an Fn::Sub variable and is left as a literal. pub unsubstituted_variables: Vec, + /// References whose target does not resolve to any resource or parameter, each with its path and unresolved target. pub invalid_refs: Vec, } +/// A property path paired with a variable name referenced there. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -180,6 +241,7 @@ pub struct PathVariable { pub variable: String, } +/// A property that resolves to null in one branch of a condition. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -187,18 +249,24 @@ pub struct PathVariable { pub struct ConditionalNull { pub path: String, pub condition: String, + /// True if the property is null in the condition's true branch; false if null in the false branch. pub null_in_true: bool, } +/// An Fn::ForEach loop within a resource. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] pub struct DiagnosticForEachExpansion { + /// Property path where the Fn::ForEach occurs. pub path: String, + /// The loop's iteration variable name. pub identifier: String, + /// A description of the collection the loop iterates over, such as a reference to a parameter, an inline list, or a dynamic value. pub collection: String, } +/// A property path paired with the reference target found there. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -207,57 +275,69 @@ pub struct PathTarget { pub target: String, } +/// An Fn::GetAtt reference to a resource attribute. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct GetAttRef { + /// Logical ID of the resource whose attribute is referenced. pub resource: String, pub attribute: String, } +/// A template output after intrinsic resolution, with its references. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct DiagnosticOutput { + /// The output value with intrinsics resolved where possible. pub value: JsonValue, #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub description: Option, + /// Name of the condition gating whether this output is emitted, if any. #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub condition: Option, #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub export_name: Option, + /// Fn::GetAtt references appearing in the output value. pub getatt_refs: Vec, + /// Names of conditions referenced within the output value. pub condition_refs: Vec, } +/// An entry from the template's Rules section. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct DiagnosticRule { pub name: String, + /// The rule's RuleCondition expression, if present, that gates when the assertions apply. #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub condition: Option, pub assertions: Vec, } +/// A single assertion within a template rule. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct DiagnosticRuleAssertion { + /// The assertion expression that must evaluate to true. pub assert_expr: JsonValue, #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub assert_description: Option, } +/// Records where a resolved resource property value came from. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -265,5 +345,6 @@ pub struct DiagnosticRuleAssertion { pub struct ResolutionSource { pub resource_id: String, pub property_path: String, + /// Origin of the resolved value, such as a parameter default, allowed values, an override, or an intrinsic. pub source: String, } diff --git a/src/template-model/src/model.rs b/src/template-model/src/model.rs index d83b427..754a506 100644 --- a/src/template-model/src/model.rs +++ b/src/template-model/src/model.rs @@ -12,42 +12,58 @@ use std::collections::{HashMap, HashSet}; use std::sync::Mutex; use std::sync::atomic::{AtomicU64, Ordering}; +/// A resource property path paired with a string value found at it, such as a substitution variable or literal. #[derive(Debug, Clone, Serialize, Default)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct PathValuePair { + /// Dot-separated property path within the resource (e.g. 'Properties.BucketName'). pub path: String, pub value: String, } +/// A property that is dropped (resolves to AWS::NoValue) in one branch of an Fn::If condition. #[derive(Debug, Clone, Serialize, Default)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ConditionalNullEntry { + /// Dot-separated property path that becomes absent in one branch. pub path: String, + /// Name of the condition governing the Fn::If that drops this property. pub condition: String, + /// True when the property is absent in the condition's true branch; false when absent in the false branch. pub null_in_true_branch: bool, } +/// Per-resource observations collected while resolving intrinsics, used to drive lint checks. #[derive(Debug, Clone, Serialize, Default)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ResourceDiagnostics { + /// Mapping names referenced by Fn::FindInMap within this resource. pub find_in_map_refs: Vec, + /// Fn::Sub uses whose template is a single variable that could be a plain Ref; each pairs the property path with the variable name. pub simple_subs: Vec, + /// Property paths where Fn::Sub wraps a constant string with no variables to substitute. pub redundant_subs: Vec, + /// Property paths where Fn::Join uses an empty delimiter, concatenating its elements directly. pub empty_joins: Vec, + /// Names of conditions referenced by this resource's property values. pub condition_refs: Vec, + /// Property paths building ARNs with a hardcoded 'aws' partition instead of AWS::Partition. pub hardcoded_partition_arns: Vec, pub conditionally_null_props: Vec, pub foreach_expansions: Vec, + /// Occurrences of ${...} placeholders outside an Fn::Sub that will not be substituted; each pairs the property path with the placeholder text. pub unsubstituted_variables: Vec, + /// References whose target is not a defined resource, parameter, or pseudo parameter; each pairs the property path with the missing target name. pub invalid_refs: Vec, } +/// A template resource with its metadata and properties after intrinsic functions have been resolved. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -77,23 +93,27 @@ pub struct ResolvedResource { #[cfg_attr(feature = "wasm-bindings", tsify(type = "Record"))] pub properties: HashMap, /// True when the entire `Properties` block is a non-map intrinsic (e.g. - /// `Properties: !Ref AWS::NoValue`) whose effective property set is decided at - /// deploy time. Distinguishes "no properties given" from "properties are - /// dynamic", so required-property checks can be skipped for the latter. + /// `Properties: !Ref AWS::NoValue`) whose effective property set is only known + /// at deploy time, as distinct from a resource that simply declares no properties. pub properties_dynamic: bool, pub diagnostics: ResourceDiagnostics, } +/// An Fn::ForEach loop within a resource that expands a property over a collection. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ForEachExpansion { + /// Property path within the resource where the Fn::ForEach appears. pub property_path: String, + /// Loop variable name bound to each element during expansion. pub identifier: String, + /// Human-readable description of the collection being iterated over. pub collection_source: String, } +/// A template output with its value and metadata after intrinsic functions have been resolved. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] @@ -174,6 +194,8 @@ pub struct SemanticModel { scenario_combinations_used: AtomicU64, } +/// Values used for AWS pseudo parameters (Ref AWS::Region, AWS::AccountId, ...) when +/// resolving the template; any field left unset falls back to a sensible default. #[derive(Debug, Clone, Default, Serialize, serde::Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm-bindings", tsify(from_wasm_abi))] diff --git a/src/template-model/src/resolver.rs b/src/template-model/src/resolver.rs index 1edf661..3dcdf75 100644 --- a/src/template-model/src/resolver.rs +++ b/src/template-model/src/resolver.rs @@ -7,40 +7,32 @@ use log::{debug, warn}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; +/// A property value after CloudFormation intrinsics (Ref, Fn::GetAtt, Fn::Sub, Fn::If, ...) +/// have been resolved as far as possible. Depending on how much can be known before +/// deployment, a value is fully concrete, a reference to another resource, one of several +/// possible values, conditional on a template condition, or opaque until deploy time. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))] pub enum ResolvedValue { - Concrete { - value: JsonValue, - }, + /// A fully known literal value (string, number, boolean, list, or object). + Concrete { value: JsonValue }, + /// A list whose elements are not all concrete; each element is itself a resolved value. #[cfg_attr(feature = "uniffi-bindings", uniffi(name = "ListValue"))] - List { - items: Vec, - }, - Map { - entries: Vec, - }, + List { items: Vec }, + /// A map whose entry values are not all concrete; each entry value is itself a resolved value. + Map { entries: Vec }, + /// One of several possible values, such as the AllowedValues of a parameter; each candidate is a resolved value. #[cfg_attr(feature = "uniffi-bindings", uniffi(name = "EnumValue"))] - Enum { - variants: Vec, - }, - Conditional { - condition: String, - if_true: Box, - if_false: Box, - }, - Reference { - target: String, - kind: RefKind, - }, - Dynamic { - reason: String, - }, - TypedDynamic { - reason: String, - param_type: String, - }, + Enum { variants: Vec }, + /// A value that depends on a template condition: `if_true` when the named condition holds, `if_false` otherwise. + Conditional { condition: String, if_true: Box, if_false: Box }, + /// A reference to another resource (via Ref or Fn::GetAtt) rather than a concrete value. + Reference { target: String, kind: RefKind }, + /// A value that cannot be known until deployment; `reason` is a human-readable explanation of why it is unresolved. + Dynamic { reason: String }, + /// A value unknown until deployment but whose CloudFormation type is known; `param_type` is that type and `reason` explains why the value is unresolved. + TypedDynamic { reason: String, param_type: String }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -51,14 +43,22 @@ pub struct MapEntry { pub value: ResolvedValue, } +/// How one template item refers to another: a Ref, an Fn::GetAtt attribute lookup, +/// an Fn::Sub variable, or an explicit DependsOn. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum RefKind { Ref, - GetAtt { attr: String }, - Sub { var: String }, + /// An Fn::GetAtt reference; `attr` is the referenced attribute name. + GetAtt { + attr: String, + }, + /// An Fn::Sub reference; `var` is the substituted variable name. + Sub { + var: String, + }, DependsOn, } @@ -72,11 +72,14 @@ pub struct ResolverEdge { pub condition_context: Option, } +/// A template Parameter's declaration: its type, constraints (allowed values/pattern, +/// length and value bounds), default, and description. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))] #[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct ParameterInfo { + /// The parameter's declared CloudFormation type (for example String, Number, or an AWS-specific type); defaults to String when the template omits Type. pub param_type: String, #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] @@ -102,10 +105,13 @@ pub struct ParameterInfo { #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub description: Option, + /// Whether the parameter is declared with NoEcho, meaning its value is masked in CloudFormation output. pub no_echo: bool, + /// Whether the AllowedPattern is a valid, supported regular expression; absent when no AllowedPattern is declared. #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub allowed_pattern_valid: Option, + /// Whether the Default value satisfies the AllowedPattern; absent unless both a Default and an AllowedPattern are declared. #[cfg_attr(feature = "wasm-bindings", tsify(optional))] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub default_matches_allowed_pattern: Option, diff --git a/src/validation-engine/src/engine.rs b/src/validation-engine/src/engine.rs index b7a51e1..8714c44 100644 --- a/src/validation-engine/src/engine.rs +++ b/src/validation-engine/src/engine.rs @@ -122,7 +122,7 @@ pub struct EngineConfig { #[serde(default)] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub custom_rules: Vec, - /// Guard DSL rules as raw source text — each engine parses and translates internally. + /// Guard DSL rules as raw source text, usable regardless of the selected engine. #[serde(default)] #[cfg_attr(feature = "uniffi-bindings", uniffi(default))] pub guard_rules: Vec, @@ -137,8 +137,8 @@ pub struct ValidateConfig { pub pseudo_parameter_overrides: PseudoParameterOverrides, /// When true, Warn-severity diagnostics are upgraded to Error. pub strict: bool, - /// When true, all built-in rules (schema validation, step functions, engine rules) are - /// skipped. Only user-provided custom rules and guard rules are evaluated. + /// When true, all built-in rules + /// Only user-provided custom rules and guard rules are evaluated. pub disable_builtin_rules: bool, }