use std::collections::BTreeMap; use crank_core::{HttpMethod, RestTarget, ToolDescription, WizardState}; use crank_mapping::MappingSet; use crank_schema::Schema; use serde::{Deserialize, Deserializer, Serialize, de}; use serde_json::Value; /// The immutable contract used to normalize an OpenAPI source. These names /// deliberately travel with an import job: changing either contract must not /// silently reinterpret a pending preview. pub const NORMALIZER_VERSION: &str = "normalized-ir-v3"; pub const PROJECTION_VERSION: &str = "preview-v3"; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] pub struct SourceDigest(String); impl SourceDigest { pub fn parse(value: impl Into) -> Result { let value = value.into(); if value.len() == 64 && value .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) { Ok(Self(value)) } else { Err("source digest must be a lowercase SHA-256 hex string") } } pub fn as_str(&self) -> &str { &self.0 } } impl<'de> Deserialize<'de> for SourceDigest { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let value = String::deserialize(deserializer)?; Self::parse(value).map_err(de::Error::custom) } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SourceIdentity { pub digest: SourceDigest, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SourceSyntax { Json, Yaml, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CanonicalSourceNode { pub construct_id: String, pub location: SourceLocation, pub value: CanonicalSourceValue, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "kind", content = "value")] pub enum CanonicalSourceValue { Null, Boolean(bool), Number(String), String(String), Array(Vec), Object(BTreeMap), } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NormalizedApiMetadata { pub title: String, pub version: Option, pub description: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NormalizedPath { pub construct_id: String, pub location: SourceLocation, pub path: String, pub operation_ids: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NormalizationConfig { pub normalizer_version: String, pub projection_version: String, pub max_bytes: usize, pub max_depth: usize, pub max_nodes: usize, pub max_collection_items: usize, pub max_aliases: usize, pub max_scalar_bytes: usize, /// Maximum number of reference hops followed from one source location. pub max_reference_depth: usize, /// Maximum number of `$ref` occurrences inspected across the bundle. pub max_references: usize, /// Maximum number of immutable external documents supplied to the pure resolver. pub max_reference_documents: usize, /// Maximum number of nodes copied while expanding resolved references. pub max_expanded_nodes: usize, pub max_external_document_bytes: usize, /// Signals that orchestration enabled external fetching. It never permits /// I/O in this crate; it only distinguishes default-deny from a missing or /// rejected supplied snapshot in exact findings. pub external_references_enabled: bool, } impl Default for NormalizationConfig { fn default() -> Self { Self { normalizer_version: NORMALIZER_VERSION.to_owned(), projection_version: PROJECTION_VERSION.to_owned(), max_bytes: 256 * 1024, max_depth: 64, max_nodes: 50_000, max_collection_items: 10_000, max_aliases: 128, max_scalar_bytes: 256 * 1024, max_reference_depth: 32, max_references: 4_096, max_reference_documents: 32, max_expanded_nodes: 100_000, max_external_document_bytes: 256 * 1024, external_references_enabled: false, } } } impl NormalizationConfig { pub fn validate_versions(&self) -> Result<(), &'static str> { if self.normalizer_version == NORMALIZER_VERSION && self.projection_version == PROJECTION_VERSION { Ok(()) } else { Err("unsupported normalization contract version") } } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SourceLocation { /// RFC 6901 JSON Pointer. It is intentionally the only source location /// exposed by the normalizer: no excerpts or parser diagnostics leak. pub pointer: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct UnresolvedReference { pub uri: String, pub location: SourceLocation, } /// Immutable external input for the pure reference resolver. The caller owns /// URL policy and I/O; `crank-import` only consumes already verified bytes. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExternalDocumentSnapshot { pub canonical_uri: String, pub digest: SourceDigest, pub document: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ResolvedReferenceNode { pub snapshot_digest: SourceDigest, pub location: SourceLocation, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ResolvedReferenceEdge { pub source: ResolvedReferenceNode, pub target: ResolvedReferenceNode, pub recursive: bool, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ResolvedReferenceGraph { /// Sorted, deduplicated immutable dependency identities. Canonical URLs /// deliberately do not enter the IR or public diagnostics. #[serde(default)] pub dependency_digests: Vec, #[serde(default)] pub edges: Vec, } /// An unresolved `$ref` preserved from the decoded source. This is deliberately /// broader than schema references: path items, reusable parameters and other /// object-level references remain available to a later resolution phase. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NormalizedReference { pub construct_id: String, pub uri: String, pub location: SourceLocation, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NormalizedFinding { pub code: String, pub severity: ImportFindingSeverity, pub message: String, pub construct_id: String, pub location: SourceLocation, #[serde(default, skip_serializing_if = "Option::is_none")] pub operation_key: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CoverageDisposition { Mapped, Finding, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CoverageEntry { pub construct_id: String, pub location: SourceLocation, pub disposition: CoverageDisposition, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct NormalizedOperation { pub stable_id: String, pub location: SourceLocation, pub key: String, pub method: HttpMethod, pub path: String, pub operation_id: Option, pub summary: Option, pub description: Option, #[serde(default)] pub tags: Vec, #[serde(default)] pub parameters: Vec, pub request_body_schema: Option, pub response_schema: Option, #[serde(default)] pub servers: Vec, #[serde(default)] pub findings: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct NormalizedParameter { pub name: String, pub location: RestParameterLocation, pub required: bool, pub description: Option, pub schema: Option, pub construct_id: String, pub source_location: SourceLocation, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct NormalizedSchema { pub construct_id: String, pub location: SourceLocation, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub discriminator: Option, #[serde(default)] pub constraints: NormalizedSchemaConstraints, pub kind: NormalizedSchemaKind, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct NormalizedSchemaConstraints { #[serde(default, skip_serializing_if = "Option::is_none")] pub minimum: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub maximum: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub min_length: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_length: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub pattern: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NormalizedDiscriminator { pub property_name: String, #[serde(default)] pub mapping: BTreeMap, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type")] pub enum NormalizedSchemaKind { Unknown, Scalar { scalar_type: NormalizedScalarKind, format: Option, nullable: bool, default_value: Option, enum_values: Vec, }, Object { properties: BTreeMap, required: Vec, }, Array { items: Option>, }, Reference { reference: UnresolvedReference, }, Composition { operator: String, variants: Vec, }, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum NormalizedScalarKind { String, Integer, Number, Boolean, Null, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "kind", content = "value")] pub enum NormalizedLiteral { String(String), Integer(i64), Unsigned(u64), Number(f64), Boolean(bool), Null, } /// Pure, canonical representation between parsing and preview projection. /// `operations`, `findings` and `coverage` are sorted before construction. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct NormalizedIr { pub normalizer_version: String, pub projection_version: String, pub source_identity: SourceIdentity, pub source_syntax: SourceSyntax, pub source_tree: CanonicalSourceNode, pub metadata: NormalizedApiMetadata, #[serde(default)] pub base_path_candidates: Vec, #[serde(default)] pub paths: Vec, #[serde(default)] pub unresolved_references: Vec, #[serde(default)] pub reference_graph: ResolvedReferenceGraph, pub source: ImportSourcePreview, #[serde(default)] pub operations: Vec, #[serde(default)] pub findings: Vec, #[serde(default)] pub coverage: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ImportFindingSeverity { Info, Warning, Error, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ImportFinding { pub code: String, pub severity: ImportFindingSeverity, pub message: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub operation_key: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ImportSourcePreview { pub format: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub version: Option, pub title: String, #[serde(default)] pub servers: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ImportPreview { pub source: ImportSourcePreview, pub groups: Vec, #[serde(default)] pub findings: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ImportGroupPreview { pub key: String, pub title: String, pub operations: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ImportOperationCandidate { pub key: String, pub method: HttpMethod, pub path: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub operation_id: Option, pub suggested_name: String, pub suggested_display_name: String, pub description: String, pub category: String, pub input_fields: usize, pub output_fields: usize, #[serde(default)] pub server_urls: Vec, #[serde(default)] pub findings: Vec, pub draft: RestImportCandidate, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RestImportCandidate { pub name: String, pub display_name: String, pub category: String, pub target: RestTarget, pub input_schema: Schema, pub output_schema: Schema, pub input_mapping: MappingSet, pub output_mapping: MappingSet, pub tool_description: ToolDescription, pub wizard_state: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RestImportDocument { pub format: String, pub version: Option, pub title: String, pub servers: Vec, pub operations: Vec, pub findings: Vec, #[serde(skip)] pub internal_finding_locations: Vec>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RestImportOperation { pub key: String, pub method: HttpMethod, pub path: String, pub operation_id: Option, pub summary: Option, pub description: Option, pub tags: Vec, pub parameters: Vec, pub request_body_schema: Option, #[serde(skip)] pub request_body_schema_location: Option, pub response_schema: Option, #[serde(skip)] pub response_schema_location: Option, pub servers: Vec, pub findings: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RestImportParameter { pub name: String, pub location: RestParameterLocation, pub required: bool, pub description: Option, pub schema: Option, #[serde(skip, default = "empty_source_location")] pub source_location: SourceLocation, #[serde(skip, default)] pub schema_source_location: Option, } fn empty_source_location() -> SourceLocation { SourceLocation { pointer: String::new(), } } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RestParameterLocation { Path, Query, Header, }