use serde_json::{Map, Value};
use super::http::RegistryClientError;
use super::types::{
AcquiredRegistrySkill, ProfileMode, RegistryAttestation, RegistryManifestSignature,
RegistryManifestSigner, RegistryPackageFile, RegistryPublisher, RegistrySearchResult,
RegistrySignedManifest, RegistrySkillDetail, RegistrySourceMetadata, TrustTier,
};
pub(crate) fn parse_search(
route: &str,
payload: &Value,
) -> Result<Vec<RegistrySearchResult>, RegistryClientError> {
let record = object(payload, route, "$")?;
require_literal_status(record, route)?;
let skills = array(required(record, "skills", route, "$")?, route, "$.skills")?;
skills
.iter()
.enumerate()
.map(|entry| {
let path = format!("$.skills[{entry}]", entry = entry.0);
let skill = object(entry.1, route, &path)?;
Ok(RegistrySearchResult {
skill_id: string_field(skill, "skill_id", route, &path)?,
name: string_field(skill, "name", route, &path)?,
summary: optional_string_field(skill, "description", route, &path)?,
category: optional_string_field(skill, "category", route, &path)?,
source_category: optional_string_field(skill, "source_category", route, &path)?,
owner: string_field(skill, "owner", route, &path)?,
version: optional_string_field(skill, "version", route, &path)?,
digest: optional_string_field(skill, "digest", route, &path)?,
source: optional_string_field(skill, "source", route, &path)?,
source_label: optional_string_field(skill, "source_label", route, &path)?,
source_type: string_field(skill, "source_type", route, &path)?,
profile_mode: profile_mode_field(skill, "profile_mode", route, &path)?,
runner_names: string_array_field(skill, "runner_names", route, &path)?,
profile_digest: optional_string_field(skill, "profile_digest", route, &path)?,
profile_trust_tier: optional_trust_tier_field(
skill,
"profile_trust_tier",
route,
&path,
)?,
required_scopes: string_array_field(skill, "required_scopes", route, &path)?,
tags: string_array_field(skill, "tags", route, &path)?,
trust_tier: trust_tier_field(skill, "trust_tier", route, &path)?,
trust_signals: trust_signals_field(skill, "trust_signals", route, &path)?,
install_command: string_field(skill, "install_command", route, &path)?,
run_command: string_field(skill, "run_command", route, &path)?,
})
})
.collect()
}
pub(crate) fn parse_read(
route: &str,
payload: &Value,
) -> Result<RegistrySkillDetail, RegistryClientError> {
let record = object(payload, route, "$")?;
require_literal_status(record, route)?;
let skill = object(required(record, "skill", route, "$")?, route, "$.skill")?;
Ok(RegistrySkillDetail {
skill_id: string_field(skill, "skill_id", route, "$.skill")?,
owner: string_field(skill, "owner", route, "$.skill")?,
name: string_field(skill, "name", route, "$.skill")?,
description: optional_string_field(skill, "description", route, "$.skill")?,
category: optional_string_field(skill, "category", route, "$.skill")?,
source_category: optional_string_field(skill, "source_category", route, "$.skill")?,
version: string_field(skill, "version", route, "$.skill")?,
digest: string_field(skill, "digest", route, "$.skill")?,
signed_manifest: signed_manifest_field(skill, "signed_manifest", route, "$.skill")?,
markdown: string_field(skill, "markdown", route, "$.skill")?,
profile_digest: optional_string_field(skill, "profile_digest", route, "$.skill")?,
package_files: package_files_field(skill, "package_files", route, "$.skill")?,
package_digest: optional_string_field(skill, "package_digest", route, "$.skill")?,
runner_names: string_array_field(skill, "runner_names", route, "$.skill")?,
source_type: string_field(skill, "source_type", route, "$.skill")?,
trust_tier: trust_tier_field(skill, "trust_tier", route, "$.skill")?,
required_scopes: string_array_field(skill, "required_scopes", route, "$.skill")?,
tags: string_array_field(skill, "tags", route, "$.skill")?,
publisher: publisher_field(skill, "publisher", route, "$.skill")?,
source_metadata: source_metadata_field(skill, "source_metadata", route, "$.skill")?,
attestations: attestations_field(skill, "attestations", route, "$.skill")?,
install_command: string_field(skill, "install_command", route, "$.skill")?,
run_command: string_field(skill, "run_command", route, "$.skill")?,
})
}
pub(crate) fn parse_acquire(
route: &str,
payload: &Value,
) -> Result<AcquiredRegistrySkill, RegistryClientError> {
let record = object(payload, route, "$")?;
require_literal_status(record, route)?;
let install_count = u64_field(record, "install_count", route, "$")?;
let acquisition = object(
required(record, "acquisition", route, "$")?,
route,
"$.acquisition",
)?;
Ok(AcquiredRegistrySkill {
skill_id: string_field(acquisition, "skill_id", route, "$.acquisition")?,
owner: string_field(acquisition, "owner", route, "$.acquisition")?,
name: string_field(acquisition, "name", route, "$.acquisition")?,
version: string_field(acquisition, "version", route, "$.acquisition")?,
digest: string_field(acquisition, "digest", route, "$.acquisition")?,
signed_manifest: signed_manifest_field(
acquisition,
"signed_manifest",
route,
"$.acquisition",
)?,
markdown: string_field(acquisition, "markdown", route, "$.acquisition")?,
profile_document: optional_string_field(
acquisition,
"profile_document",
route,
"$.acquisition",
)?,
profile_digest: optional_string_field(
acquisition,
"profile_digest",
route,
"$.acquisition",
)?,
package_files: package_files_field(acquisition, "package_files", route, "$.acquisition")?,
package_digest: optional_string_field(
acquisition,
"package_digest",
route,
"$.acquisition",
)?,
runner_names: string_array_field(acquisition, "runner_names", route, "$.acquisition")?,
trust_tier: trust_tier_field(acquisition, "trust_tier", route, "$.acquisition")?,
publisher: publisher_field(acquisition, "publisher", route, "$.acquisition")?,
source_metadata: source_metadata_field(
acquisition,
"source_metadata",
route,
"$.acquisition",
)?,
attestations: attestations_field(acquisition, "attestations", route, "$.acquisition")?,
install_count,
})
}
fn require_literal_status(
record: &Map<String, Value>,
route: &str,
) -> Result<(), RegistryClientError> {
match record.get("status").and_then(Value::as_str) {
Some("success") => Ok(()),
Some(_) | None => Err(contract_error(
route,
"$.status",
"expected literal 'success'",
)),
}
}
fn required<'a>(
record: &'a Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<&'a Value, RegistryClientError> {
record
.get(field)
.ok_or_else(|| contract_error(route, &format!("{path}.{field}"), "missing required field"))
}
fn object<'a>(
value: &'a Value,
route: &str,
path: &str,
) -> Result<&'a Map<String, Value>, RegistryClientError> {
value
.as_object()
.ok_or_else(|| contract_error(route, path, "expected object"))
}
fn array<'a>(
value: &'a Value,
route: &str,
path: &str,
) -> Result<&'a Vec<Value>, RegistryClientError> {
value
.as_array()
.ok_or_else(|| contract_error(route, path, "expected array"))
}
fn string_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<String, RegistryClientError> {
required(record, field, route, path)?
.as_str()
.map(ToOwned::to_owned)
.ok_or_else(|| contract_error(route, &format!("{path}.{field}"), "expected string"))
}
fn optional_string_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Option<String>, RegistryClientError> {
match record.get(field) {
None | Some(Value::Null) => Ok(None),
Some(value) => value
.as_str()
.map(|inner| Some(inner.to_owned()))
.ok_or_else(|| contract_error(route, &format!("{path}.{field}"), "expected string")),
}
}
fn u64_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<u64, RegistryClientError> {
required(record, field, route, path)?
.as_u64()
.ok_or_else(|| {
contract_error(
route,
&format!("{path}.{field}"),
"expected unsigned integer",
)
})
}
fn bool_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<bool, RegistryClientError> {
required(record, field, route, path)?
.as_bool()
.ok_or_else(|| contract_error(route, &format!("{path}.{field}"), "expected boolean"))
}
fn string_array_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Vec<String>, RegistryClientError> {
let base = format!("{path}.{field}");
array(required(record, field, route, path)?, route, &base)?
.iter()
.enumerate()
.map(|(index, value)| {
value.as_str().map(ToOwned::to_owned).ok_or_else(|| {
contract_error(route, &format!("{base}[{index}]"), "expected string")
})
})
.collect()
}
fn package_files_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Vec<RegistryPackageFile>, RegistryClientError> {
let Some(value) = record.get(field) else {
return Ok(Vec::new());
};
let base = format!("{path}.{field}");
array(value, route, &base)?
.iter()
.enumerate()
.map(|(index, value)| {
let file = object(value, route, &format!("{base}[{index}]"))?;
Ok(RegistryPackageFile {
path: string_field(file, "path", route, &format!("{base}[{index}]"))?,
content: string_field(file, "content", route, &format!("{base}[{index}]"))?,
})
})
.collect()
}
fn trust_tier_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<TrustTier, RegistryClientError> {
match string_field(record, field, route, path)?.as_str() {
"first_party" => Ok(TrustTier::FirstParty),
"verified" => Ok(TrustTier::Verified),
"community" => Ok(TrustTier::Community),
_ => Err(contract_error(
route,
&format!("{path}.{field}"),
"expected first_party, verified, or community",
)),
}
}
fn optional_trust_tier_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Option<TrustTier>, RegistryClientError> {
match record.get(field) {
None | Some(Value::Null) => Ok(None),
Some(_) => trust_tier_field(record, field, route, path).map(Some),
}
}
fn profile_mode_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<ProfileMode, RegistryClientError> {
match string_field(record, field, route, path)?.as_str() {
"portable" => Ok(ProfileMode::Portable),
"profiled" => Ok(ProfileMode::Profiled),
_ => Err(contract_error(
route,
&format!("{path}.{field}"),
"expected portable or profiled",
)),
}
}
fn trust_signals_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Vec<super::types::TrustSignal>, RegistryClientError> {
let Some(value) = record.get(field) else {
return Ok(Vec::new());
};
if value.is_null() {
return Ok(Vec::new());
}
let base = format!("{path}.{field}");
array(value, route, &base)?
.iter()
.enumerate()
.map(|(index, value)| {
let item_path = format!("{base}[{index}]");
let item = object(value, route, &item_path)?;
Ok(super::types::TrustSignal {
id: string_field(item, "id", route, &item_path)?,
label: string_field(item, "label", route, &item_path)?,
status: string_field(item, "status", route, &item_path)?,
value: string_field(item, "value", route, &item_path)?,
})
})
.collect()
}
fn publisher_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<RegistryPublisher, RegistryClientError> {
let field_path = format!("{path}.{field}");
let publisher = object(required(record, field, route, path)?, route, &field_path)?;
Ok(RegistryPublisher {
kind: string_field(publisher, "kind", route, &field_path)?,
id: string_field(publisher, "id", route, &field_path)?,
handle: optional_string_field(publisher, "handle", route, &field_path)?,
display_name: optional_string_field(publisher, "display_name", route, &field_path)?,
})
}
fn signed_manifest_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Option<RegistrySignedManifest>, RegistryClientError> {
let Some(value) = record.get(field) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let field_path = format!("{path}.{field}");
let manifest = object(value, route, &field_path)?;
let signer_path = format!("{field_path}.signer");
let signer = object(
required(manifest, "signer", route, &field_path)?,
route,
&signer_path,
)?;
let signature_path = format!("{field_path}.signature");
let signature = object(
required(manifest, "signature", route, &field_path)?,
route,
&signature_path,
)?;
Ok(Some(RegistrySignedManifest {
schema: string_field(manifest, "schema", route, &field_path)?,
skill_id: string_field(manifest, "skill_id", route, &field_path)?,
version: string_field(manifest, "version", route, &field_path)?,
digest: string_field(manifest, "digest", route, &field_path)?,
profile_digest: optional_string_field(manifest, "profile_digest", route, &field_path)?,
package_digest: optional_string_field(manifest, "package_digest", route, &field_path)?,
signer: RegistryManifestSigner {
id: string_field(signer, "id", route, &signer_path)?,
key_id: string_field(signer, "key_id", route, &signer_path)?,
},
signature: RegistryManifestSignature {
alg: string_field(signature, "alg", route, &signature_path)?,
value: string_field(signature, "value", route, &signature_path)?,
},
}))
}
fn attestations_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Vec<RegistryAttestation>, RegistryClientError> {
let base = format!("{path}.{field}");
array(required(record, field, route, path)?, route, &base)?
.iter()
.enumerate()
.map(|(index, value)| {
let item_path = format!("{base}[{index}]");
let item = object(value, route, &item_path)?;
Ok(RegistryAttestation {
kind: string_field(item, "kind", route, &item_path)?,
id: string_field(item, "id", route, &item_path)?,
status: string_field(item, "status", route, &item_path)?,
summary: string_field(item, "summary", route, &item_path)?,
source: optional_string_field(item, "source", route, &item_path)?,
issued_at: optional_string_field(item, "issued_at", route, &item_path)?,
metadata: optional_json_field(item, "metadata", route, &item_path)?,
})
})
.collect()
}
fn source_metadata_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Option<RegistrySourceMetadata>, RegistryClientError> {
let Some(value) = record.get(field) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let field_path = format!("{path}.{field}");
let source = object(value, route, &field_path)?;
Ok(Some(RegistrySourceMetadata {
provider: string_field(source, "provider", route, &field_path)?,
repo: string_field(source, "repo", route, &field_path)?,
repo_url: string_field(source, "repo_url", route, &field_path)?,
skill_path: string_field(source, "skill_path", route, &field_path)?,
profile_path: optional_string_field(source, "profile_path", route, &field_path)?,
r#ref: string_field(source, "ref", route, &field_path)?,
sha: string_field(source, "sha", route, &field_path)?,
default_branch: string_field(source, "default_branch", route, &field_path)?,
event: string_field(source, "event", route, &field_path)?,
immutable: bool_field(source, "immutable", route, &field_path)?,
live: bool_field(source, "live", route, &field_path)?,
tombstoned: optional_bool_field(source, "tombstoned", route, &field_path)?,
tag: optional_string_field(source, "tag", route, &field_path)?,
publisher_handle: optional_string_field(source, "publisher_handle", route, &field_path)?,
}))
}
fn optional_bool_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Option<bool>, RegistryClientError> {
match record.get(field) {
None | Some(Value::Null) => Ok(None),
Some(value) => value
.as_bool()
.map(Some)
.ok_or_else(|| contract_error(route, &format!("{path}.{field}"), "expected boolean")),
}
}
fn optional_json_field(
record: &Map<String, Value>,
field: &str,
route: &str,
path: &str,
) -> Result<Option<runx_contracts::JsonValue>, RegistryClientError> {
match record.get(field) {
None | Some(Value::Null) => Ok(None),
Some(value) => serde_json::from_value(value.clone())
.map(Some)
.map_err(|error| contract_error(route, &format!("{path}.{field}"), &error.to_string())),
}
}
fn contract_error(route: &str, field_path: &str, message: &str) -> RegistryClientError {
RegistryClientError::Contract {
route: route.to_owned(),
field_path: field_path.to_owned(),
message: message.to_owned(),
}
}