rustev_contract/
profile.rs1use crate::{
3 Document, DocumentError, Identified,
4 bounded::{ParseError, parse_bounded},
5 ids::ProfileId,
6 limits::DESCRIPTOR_V1,
7 parameter::MAX_PARAMETERS,
8 schema,
9 version::{Version, VersionRange},
10};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::{collections::BTreeMap, fmt};
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
16#[serde(deny_unknown_fields)]
17pub struct Profile {
18 pub schema: String,
19 pub profile: String,
20 pub version: String,
21 pub definition: ProfileDefinition,
22 pub extends: String,
23 pub values: BTreeMap<String, Value>,
24}
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct ProfileDefinition {
28 pub name: String,
29 pub version: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ProfileError {
35 pub member: &'static str,
36}
37impl fmt::Display for ProfileError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 write!(f, "invalid profile member {}", self.member)
40 }
41}
42impl std::error::Error for ProfileError {}
43pub fn is_profile_name(s: &str) -> bool {
45 (1..=64).contains(&s.len())
46 && s.as_bytes()[0].is_ascii_alphanumeric()
47 && s.bytes()
48 .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
49}
50pub fn is_profile_extends(s: &str) -> bool {
52 s == "none"
53 || s.split_once('@')
54 .is_some_and(|(n, v)| is_profile_name(n) && v.parse::<Version>().is_ok())
55}
56impl Profile {
57 pub fn validate(&self) -> Result<(), ProfileError> {
58 for (member, valid) in [
59 ("schema", self.schema == schema::PROFILE),
60 ("profile", is_profile_name(&self.profile)),
61 ("version", self.version.parse::<Version>().is_ok()),
62 (
63 "definition.version",
64 self.definition.version.parse::<VersionRange>().is_ok(),
65 ),
66 ("extends", is_profile_extends(&self.extends)),
67 ("values", self.values.len() <= MAX_PARAMETERS),
68 ] {
69 if !valid {
70 return Err(ProfileError { member });
71 }
72 }
73 Ok(())
74 }
75}
76impl Document for Profile {
77 const SCHEMA: &'static str = schema::PROFILE;
78 const LIMITS: crate::bounded::ParseLimits = DESCRIPTOR_V1;
79 fn schema(&self) -> &str {
80 &self.schema
81 }
82 fn parse(bytes: &[u8]) -> Result<Self, DocumentError> {
83 let doc: Self = parse_bounded(bytes, &Self::LIMITS).map_err(DocumentError::Parse)?;
84 if doc.schema != Self::SCHEMA {
85 return Err(DocumentError::Schema {
86 expected: Self::SCHEMA,
87 found: doc.schema.chars().take(80).collect(),
88 });
89 }
90 doc.validate().map_err(|e| {
91 DocumentError::Parse(ParseError::Schema {
92 message: e.to_string(),
93 })
94 })?;
95 Ok(doc)
96 }
97}
98impl Identified for Profile {
99 type Id = ProfileId;
100 fn wrap(digest: String) -> ProfileId {
101 ProfileId::from_digest(digest)
102 }
103}