lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

registry_v7.rs (36856B)


      1 #[cfg(not(feature = "std"))]
      2 use alloc::{
      3     format,
      4     string::{String, ToString},
      5     vec::Vec,
      6 };
      7 
      8 use radroots_event::{
      9     contract::{
     10         AuthorRole, ContentSchema, EventAuthoringPolicy, EventClass, EventContract,
     11         EventDiscriminator, EventPrivacy, EventStability, KindContract, NostrStandard,
     12         RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, Reducer, TagCardinality, TagContract,
     13         TagSemantic, TagValueType, all_event_contracts_registry_v7, all_kind_contracts_registry_v7,
     14     },
     15     listing::classified::ClassifiedListingPartition,
     16 };
     17 use serde::{Deserialize, Serialize};
     18 
     19 use super::{canonical_manifest_json, manifest_sha256, parse_manifest_json};
     20 
     21 pub const RADROOTS_EVENT_CONTRACT_REGISTRY_V7_INVENTORY_SCHEMA_VERSION: u32 = 1;
     22 pub const RADROOTS_EVENT_CONTRACT_REGISTRY_V7_VERSION: u32 =
     23     RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION;
     24 pub const RADROOTS_EVENT_CONTRACT_REGISTRY_V7_KIND_COUNT: usize =
     25     all_kind_contracts_registry_v7().len();
     26 pub const RADROOTS_EVENT_CONTRACT_REGISTRY_V7_EVENT_COUNT: usize =
     27     all_event_contracts_registry_v7().len();
     28 
     29 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     30 #[serde(deny_unknown_fields)]
     31 pub struct RadrootsEventContractRegistryV7Inventory {
     32     pub schema_version: u32,
     33     pub event_contract_registry_version: u32,
     34     pub kind_contracts: Vec<RadrootsKindContractRegistryV7InventoryEntry>,
     35     pub event_contracts: Vec<RadrootsEventContractRegistryV7InventoryEntry>,
     36 }
     37 
     38 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     39 #[serde(deny_unknown_fields)]
     40 pub struct RadrootsKindContractRegistryV7InventoryEntry {
     41     pub ordinal: usize,
     42     pub kind: u32,
     43     pub canonical_constant: String,
     44     pub name: String,
     45     pub class: RadrootsEventContractRegistryV7Class,
     46     pub standard: RadrootsEventContractRegistryV7Standard,
     47     pub accepted_event_contracts: Vec<String>,
     48 }
     49 
     50 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
     51 #[serde(deny_unknown_fields)]
     52 pub struct RadrootsEventContractRegistryV7InventoryEntry {
     53     pub ordinal: usize,
     54     pub contract_id: String,
     55     pub kind: u32,
     56     pub name: String,
     57     pub payload_type: String,
     58     pub class: RadrootsEventContractRegistryV7Class,
     59     pub stability: RadrootsEventContractRegistryV7Stability,
     60     pub privacy: RadrootsEventContractRegistryV7Privacy,
     61     pub author_role: RadrootsEventContractRegistryV7ActorRole,
     62     pub content_schema: RadrootsEventContractRegistryV7ContentSchema,
     63     pub authoring_policy: RadrootsEventContractRegistryV7AuthoringPolicy,
     64     pub discriminator: RadrootsEventContractRegistryV7Discriminator,
     65     pub tags: Vec<RadrootsEventContractRegistryV7Tag>,
     66     pub reducers: Vec<RadrootsEventContractRegistryV7Reducer>,
     67 }
     68 
     69 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     70 #[serde(rename_all = "snake_case")]
     71 pub enum RadrootsEventContractRegistryV7Class {
     72     Regular,
     73     Replaceable,
     74     Addressable,
     75     Ephemeral,
     76 }
     77 
     78 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
     79 #[serde(rename_all = "snake_case")]
     80 pub enum RadrootsEventContractRegistryV7Standard {
     81     Nip01,
     82     Nip09,
     83     Nip17,
     84     Nip18,
     85     Nip22,
     86     Nip23,
     87     Nip25,
     88     Nip28,
     89     Nip29,
     90     Nip42,
     91     Nip51,
     92     Nip52,
     93     Nip53,
     94     Nip54,
     95     Nip56,
     96     Nip57,
     97     Nip78,
     98     Nip90,
     99     Nip94,
    100     Nip98,
    101     Nip99,
    102     Radroots,
    103 }
    104 
    105 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    106 #[serde(rename_all = "snake_case")]
    107 pub enum RadrootsEventContractRegistryV7Stability {
    108     Stable,
    109     Experimental,
    110 }
    111 
    112 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    113 #[serde(rename_all = "snake_case")]
    114 pub enum RadrootsEventContractRegistryV7Privacy {
    115     Public,
    116     Encrypted,
    117     LocalOnly,
    118     Secret,
    119 }
    120 
    121 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    122 #[serde(rename_all = "snake_case")]
    123 pub enum RadrootsEventContractRegistryV7ActorRole {
    124     Any,
    125     Application,
    126     Buyer,
    127     Farmer,
    128     Member,
    129     Moderator,
    130     Relay,
    131     Seller,
    132     Service,
    133 }
    134 
    135 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    136 #[serde(rename_all = "snake_case")]
    137 pub enum RadrootsEventContractRegistryV7ContentSchema {
    138     Empty,
    139     JsonObject,
    140     PlainText,
    141     Markdown,
    142     Djot,
    143     Encrypted,
    144     BinaryReference,
    145 }
    146 
    147 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    148 #[serde(rename_all = "snake_case")]
    149 pub enum RadrootsEventContractRegistryV7AuthoringPolicy {
    150     GenericDraft,
    151     TypedOnly,
    152     ReadOnly,
    153 }
    154 
    155 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    156 #[serde(tag = "type", rename_all = "snake_case")]
    157 pub enum RadrootsEventContractRegistryV7Discriminator {
    158     KindOnly,
    159     AdmissionOnly,
    160     ClassifiedListingPartition {
    161         value: RadrootsEventContractRegistryV7ClassifiedListingPartition,
    162     },
    163     DTagExact {
    164         value: String,
    165     },
    166     DTagPrefix {
    167         prefix: String,
    168     },
    169     DTagSuffix {
    170         suffix: String,
    171     },
    172     TagEquals {
    173         name: String,
    174         value: String,
    175     },
    176     ContentJsonFieldEquals {
    177         field: String,
    178         value: String,
    179     },
    180     EnvelopeType {
    181         value: String,
    182     },
    183     Composite {
    184         parts: Vec<RadrootsEventContractRegistryV7Discriminator>,
    185     },
    186 }
    187 
    188 impl<'de> Deserialize<'de> for RadrootsEventContractRegistryV7Discriminator {
    189     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    190     where
    191         D: serde::Deserializer<'de>,
    192     {
    193         let value = serde_json::Value::deserialize(deserializer)?;
    194         discriminator_from_json_value(&value).map_err(serde::de::Error::custom)
    195     }
    196 }
    197 
    198 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    199 #[serde(rename_all = "snake_case")]
    200 pub enum RadrootsEventContractRegistryV7ClassifiedListingPartition {
    201     FocusedFoodAvailability,
    202     OperationalListing,
    203     GenericNip99,
    204     Ambiguous,
    205 }
    206 
    207 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    208 #[serde(deny_unknown_fields)]
    209 pub struct RadrootsEventContractRegistryV7Tag {
    210     pub name: String,
    211     pub cardinality: RadrootsEventContractRegistryV7TagCardinality,
    212     pub semantic: RadrootsEventContractRegistryV7TagSemantic,
    213     pub value_type: RadrootsEventContractRegistryV7TagValueType,
    214     pub relay_indexed: bool,
    215 }
    216 
    217 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    218 #[serde(rename_all = "snake_case")]
    219 pub enum RadrootsEventContractRegistryV7TagCardinality {
    220     RequiredOne,
    221     OptionalOne,
    222     OptionalMany,
    223     RequiredMany,
    224 }
    225 
    226 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    227 #[serde(rename_all = "snake_case")]
    228 pub enum RadrootsEventContractRegistryV7TagSemantic {
    229     AddressableCoordinate,
    230     CalendarEventAuthor,
    231     CalendarEventReference,
    232     CalendarEventRevision,
    233     CalendarInclusionRequest,
    234     CalendarEnd,
    235     CalendarStart,
    236     Category,
    237     Citation,
    238     Contract,
    239     Counterparty,
    240     Evidence,
    241     EventPointer,
    242     FreeBusy,
    243     Geohash,
    244     GroupId,
    245     Identifier,
    246     Image,
    247     Kind,
    248     ClassifiedListingAddress,
    249     OperationalListingSnapshot,
    250     ListDescription,
    251     Location,
    252     Nip01Coordinate,
    253     Participant,
    254     PreviousEvent,
    255     Price,
    256     PublishedAt,
    257     Relay,
    258     Reference,
    259     ReviewTarget,
    260     RootEvent,
    261     ServiceInput,
    262     ServiceOutput,
    263     Source,
    264     Status,
    265     Summary,
    266     Title,
    267     Topic,
    268     TimeZone,
    269     TradeMutation,
    270     Url,
    271     UtcDayCoverage,
    272 }
    273 
    274 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    275 #[serde(rename_all = "snake_case")]
    276 pub enum RadrootsEventContractRegistryV7TagValueType {
    277     AddressableCoordinate,
    278     CalendarDate,
    279     CalendarEventCoordinate,
    280     CalendarFreeBusy,
    281     CalendarRsvpStatus,
    282     CalendarUid,
    283     ContractId,
    284     DTag,
    285     EventId,
    286     EventPointer,
    287     Geohash,
    288     IanaTimeZoneId,
    289     Kind,
    290     MutationId,
    291     Nip01Coordinate,
    292     PublicKey,
    293     RelayUrl,
    294     Sha256,
    295     Text,
    296     UnixTimestamp,
    297     Uri,
    298     Url,
    299     UtcDayIndex,
    300     Uuid,
    301 }
    302 
    303 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    304 #[serde(rename_all = "snake_case")]
    305 pub enum RadrootsEventContractRegistryV7Reducer {
    306     CalendarProjection,
    307     FarmOpsProjection,
    308     GroupProjection,
    309     KnowledgeProjection,
    310     OperationalListingInventoryAccounting,
    311     OperationalListingProjection,
    312     MarketProjection,
    313     OrderProjection,
    314     ProfileProjection,
    315     NostrRelayPolicyProjection,
    316     SocialProjection,
    317     TradeProjection,
    318     TradeValidation,
    319 }
    320 
    321 pub fn event_contract_registry_v7_inventory() -> RadrootsEventContractRegistryV7Inventory {
    322     RadrootsEventContractRegistryV7Inventory {
    323         schema_version: RADROOTS_EVENT_CONTRACT_REGISTRY_V7_INVENTORY_SCHEMA_VERSION,
    324         event_contract_registry_version: RADROOTS_EVENT_CONTRACT_REGISTRY_V7_VERSION,
    325         kind_contracts: all_kind_contracts_registry_v7()
    326             .iter()
    327             .enumerate()
    328             .map(|(ordinal, contract)| kind_inventory_entry(ordinal, contract))
    329             .collect(),
    330         event_contracts: all_event_contracts_registry_v7()
    331             .iter()
    332             .enumerate()
    333             .map(|(ordinal, contract)| event_inventory_entry(ordinal, contract))
    334             .collect(),
    335     }
    336 }
    337 
    338 pub fn event_contract_registry_v7_inventory_json() -> Result<String, serde_json::Error> {
    339     canonical_manifest_json(&event_contract_registry_v7_inventory())
    340 }
    341 
    342 pub fn event_contract_registry_v7_inventory_sha256() -> Result<String, serde_json::Error> {
    343     let json = event_contract_registry_v7_inventory_json()?;
    344     Ok(manifest_sha256(&json))
    345 }
    346 
    347 pub fn parse_event_contract_registry_v7_inventory_json(
    348     json: &str,
    349 ) -> Result<RadrootsEventContractRegistryV7Inventory, serde_json::Error> {
    350     parse_manifest_json(json)
    351 }
    352 
    353 fn kind_inventory_entry(
    354     ordinal: usize,
    355     contract: &KindContract,
    356 ) -> RadrootsKindContractRegistryV7InventoryEntry {
    357     RadrootsKindContractRegistryV7InventoryEntry {
    358         ordinal,
    359         kind: contract.kind,
    360         canonical_constant: contract.canonical_constant.to_string(),
    361         name: contract.name.to_string(),
    362         class: class_inventory(contract.class),
    363         standard: standard_inventory(contract.standard),
    364         accepted_event_contracts: contract
    365             .accepted_event_contracts
    366             .iter()
    367             .map(|contract_id| (*contract_id).to_string())
    368             .collect(),
    369     }
    370 }
    371 
    372 fn event_inventory_entry(
    373     ordinal: usize,
    374     contract: &EventContract,
    375 ) -> RadrootsEventContractRegistryV7InventoryEntry {
    376     RadrootsEventContractRegistryV7InventoryEntry {
    377         ordinal,
    378         contract_id: contract.id.to_string(),
    379         kind: contract.kind,
    380         name: contract.name.to_string(),
    381         payload_type: contract.payload_type.to_string(),
    382         class: class_inventory(contract.class),
    383         stability: stability_inventory(contract.stability),
    384         privacy: privacy_inventory(contract.privacy),
    385         author_role: author_role_inventory(contract.required_author_role()),
    386         content_schema: content_schema_inventory(contract.content_schema),
    387         authoring_policy: authoring_policy_inventory(contract.authoring_policy()),
    388         discriminator: discriminator_inventory(&contract.discriminator),
    389         tags: contract.tags.iter().map(tag_inventory).collect(),
    390         reducers: contract
    391             .reducers
    392             .iter()
    393             .copied()
    394             .map(reducer_inventory)
    395             .collect(),
    396     }
    397 }
    398 
    399 fn class_inventory(value: EventClass) -> RadrootsEventContractRegistryV7Class {
    400     match value {
    401         EventClass::Regular => RadrootsEventContractRegistryV7Class::Regular,
    402         EventClass::Replaceable => RadrootsEventContractRegistryV7Class::Replaceable,
    403         EventClass::Addressable => RadrootsEventContractRegistryV7Class::Addressable,
    404         EventClass::Ephemeral => RadrootsEventContractRegistryV7Class::Ephemeral,
    405     }
    406 }
    407 
    408 fn standard_inventory(value: NostrStandard) -> RadrootsEventContractRegistryV7Standard {
    409     match value {
    410         NostrStandard::Nip01 => RadrootsEventContractRegistryV7Standard::Nip01,
    411         NostrStandard::Nip09 => RadrootsEventContractRegistryV7Standard::Nip09,
    412         NostrStandard::Nip17 => RadrootsEventContractRegistryV7Standard::Nip17,
    413         NostrStandard::Nip18 => RadrootsEventContractRegistryV7Standard::Nip18,
    414         NostrStandard::Nip22 => RadrootsEventContractRegistryV7Standard::Nip22,
    415         NostrStandard::Nip23 => RadrootsEventContractRegistryV7Standard::Nip23,
    416         NostrStandard::Nip25 => RadrootsEventContractRegistryV7Standard::Nip25,
    417         NostrStandard::Nip28 => RadrootsEventContractRegistryV7Standard::Nip28,
    418         NostrStandard::Nip29 => RadrootsEventContractRegistryV7Standard::Nip29,
    419         NostrStandard::Nip42 => RadrootsEventContractRegistryV7Standard::Nip42,
    420         NostrStandard::Nip51 => RadrootsEventContractRegistryV7Standard::Nip51,
    421         NostrStandard::Nip52 => RadrootsEventContractRegistryV7Standard::Nip52,
    422         NostrStandard::Nip53 => RadrootsEventContractRegistryV7Standard::Nip53,
    423         NostrStandard::Nip54 => RadrootsEventContractRegistryV7Standard::Nip54,
    424         NostrStandard::Nip56 => RadrootsEventContractRegistryV7Standard::Nip56,
    425         NostrStandard::Nip57 => RadrootsEventContractRegistryV7Standard::Nip57,
    426         NostrStandard::Nip78 => RadrootsEventContractRegistryV7Standard::Nip78,
    427         NostrStandard::Nip90 => RadrootsEventContractRegistryV7Standard::Nip90,
    428         NostrStandard::Nip94 => RadrootsEventContractRegistryV7Standard::Nip94,
    429         NostrStandard::Nip98 => RadrootsEventContractRegistryV7Standard::Nip98,
    430         NostrStandard::Nip99 => RadrootsEventContractRegistryV7Standard::Nip99,
    431         NostrStandard::Radroots => RadrootsEventContractRegistryV7Standard::Radroots,
    432     }
    433 }
    434 
    435 fn stability_inventory(value: EventStability) -> RadrootsEventContractRegistryV7Stability {
    436     match value {
    437         EventStability::Stable => RadrootsEventContractRegistryV7Stability::Stable,
    438         EventStability::Experimental => RadrootsEventContractRegistryV7Stability::Experimental,
    439     }
    440 }
    441 
    442 fn privacy_inventory(value: EventPrivacy) -> RadrootsEventContractRegistryV7Privacy {
    443     match value {
    444         EventPrivacy::Public => RadrootsEventContractRegistryV7Privacy::Public,
    445         EventPrivacy::Encrypted => RadrootsEventContractRegistryV7Privacy::Encrypted,
    446         EventPrivacy::LocalOnly => RadrootsEventContractRegistryV7Privacy::LocalOnly,
    447         EventPrivacy::Secret => RadrootsEventContractRegistryV7Privacy::Secret,
    448     }
    449 }
    450 
    451 fn author_role_inventory(value: AuthorRole) -> RadrootsEventContractRegistryV7ActorRole {
    452     match value {
    453         AuthorRole::Any => RadrootsEventContractRegistryV7ActorRole::Any,
    454         AuthorRole::Application => RadrootsEventContractRegistryV7ActorRole::Application,
    455         AuthorRole::Buyer => RadrootsEventContractRegistryV7ActorRole::Buyer,
    456         AuthorRole::Farmer => RadrootsEventContractRegistryV7ActorRole::Farmer,
    457         AuthorRole::Member => RadrootsEventContractRegistryV7ActorRole::Member,
    458         AuthorRole::Moderator => RadrootsEventContractRegistryV7ActorRole::Moderator,
    459         AuthorRole::Relay => RadrootsEventContractRegistryV7ActorRole::Relay,
    460         AuthorRole::Seller => RadrootsEventContractRegistryV7ActorRole::Seller,
    461         AuthorRole::Service => RadrootsEventContractRegistryV7ActorRole::Service,
    462     }
    463 }
    464 
    465 fn content_schema_inventory(value: ContentSchema) -> RadrootsEventContractRegistryV7ContentSchema {
    466     match value {
    467         ContentSchema::Empty => RadrootsEventContractRegistryV7ContentSchema::Empty,
    468         ContentSchema::JsonObject => RadrootsEventContractRegistryV7ContentSchema::JsonObject,
    469         ContentSchema::PlainText => RadrootsEventContractRegistryV7ContentSchema::PlainText,
    470         ContentSchema::Markdown => RadrootsEventContractRegistryV7ContentSchema::Markdown,
    471         ContentSchema::Djot => RadrootsEventContractRegistryV7ContentSchema::Djot,
    472         ContentSchema::Encrypted => RadrootsEventContractRegistryV7ContentSchema::Encrypted,
    473         ContentSchema::BinaryReference => {
    474             RadrootsEventContractRegistryV7ContentSchema::BinaryReference
    475         }
    476     }
    477 }
    478 
    479 fn authoring_policy_inventory(
    480     value: EventAuthoringPolicy,
    481 ) -> RadrootsEventContractRegistryV7AuthoringPolicy {
    482     match value {
    483         EventAuthoringPolicy::GenericDraft => {
    484             RadrootsEventContractRegistryV7AuthoringPolicy::GenericDraft
    485         }
    486         EventAuthoringPolicy::TypedOnly => {
    487             RadrootsEventContractRegistryV7AuthoringPolicy::TypedOnly
    488         }
    489         EventAuthoringPolicy::ReadOnly => RadrootsEventContractRegistryV7AuthoringPolicy::ReadOnly,
    490     }
    491 }
    492 
    493 fn discriminator_inventory(
    494     value: &EventDiscriminator,
    495 ) -> RadrootsEventContractRegistryV7Discriminator {
    496     match value {
    497         EventDiscriminator::KindOnly => RadrootsEventContractRegistryV7Discriminator::KindOnly,
    498         EventDiscriminator::AdmissionOnly => {
    499             RadrootsEventContractRegistryV7Discriminator::AdmissionOnly
    500         }
    501         EventDiscriminator::ClassifiedListingPartition(value) => {
    502             RadrootsEventContractRegistryV7Discriminator::ClassifiedListingPartition {
    503                 value: classified_listing_partition_inventory(*value),
    504             }
    505         }
    506         EventDiscriminator::DTagExact(value) => {
    507             RadrootsEventContractRegistryV7Discriminator::DTagExact {
    508                 value: (*value).to_string(),
    509             }
    510         }
    511         EventDiscriminator::DTagPrefix(prefix) => {
    512             RadrootsEventContractRegistryV7Discriminator::DTagPrefix {
    513                 prefix: (*prefix).to_string(),
    514             }
    515         }
    516         EventDiscriminator::DTagSuffix(suffix) => {
    517             RadrootsEventContractRegistryV7Discriminator::DTagSuffix {
    518                 suffix: (*suffix).to_string(),
    519             }
    520         }
    521         EventDiscriminator::TagEquals { name, value } => {
    522             RadrootsEventContractRegistryV7Discriminator::TagEquals {
    523                 name: (*name).to_string(),
    524                 value: (*value).to_string(),
    525             }
    526         }
    527         EventDiscriminator::ContentJsonFieldEquals { field, value } => {
    528             RadrootsEventContractRegistryV7Discriminator::ContentJsonFieldEquals {
    529                 field: (*field).to_string(),
    530                 value: (*value).to_string(),
    531             }
    532         }
    533         EventDiscriminator::EnvelopeType(value) => {
    534             RadrootsEventContractRegistryV7Discriminator::EnvelopeType {
    535                 value: (*value).to_string(),
    536             }
    537         }
    538         EventDiscriminator::Composite(parts) => {
    539             RadrootsEventContractRegistryV7Discriminator::Composite {
    540                 parts: parts.iter().map(discriminator_inventory).collect(),
    541             }
    542         }
    543     }
    544 }
    545 
    546 fn discriminator_from_json_value(
    547     value: &serde_json::Value,
    548 ) -> Result<RadrootsEventContractRegistryV7Discriminator, String> {
    549     let object = value
    550         .as_object()
    551         .ok_or_else(|| "registry-v7 discriminator must be an object".to_string())?;
    552     let discriminator_type = object
    553         .get("type")
    554         .and_then(serde_json::Value::as_str)
    555         .ok_or_else(|| "registry-v7 discriminator type must be a string".to_string())?;
    556 
    557     match discriminator_type {
    558         "kind_only" => {
    559             require_exact_discriminator_fields(object, &["type"])?;
    560             Ok(RadrootsEventContractRegistryV7Discriminator::KindOnly)
    561         }
    562         "admission_only" => {
    563             require_exact_discriminator_fields(object, &["type"])?;
    564             Ok(RadrootsEventContractRegistryV7Discriminator::AdmissionOnly)
    565         }
    566         "classified_listing_partition" => {
    567             require_exact_discriminator_fields(object, &["type", "value"])?;
    568             let value = serde_json::from_value(
    569                 object
    570                     .get("value")
    571                     .cloned()
    572                     .ok_or_else(|| "registry-v7 discriminator is missing value".to_string())?,
    573             )
    574             .map_err(|error| {
    575                 format!("invalid registry-v7 classified-listing discriminator value: {error}")
    576             })?;
    577             Ok(RadrootsEventContractRegistryV7Discriminator::ClassifiedListingPartition { value })
    578         }
    579         "d_tag_exact" => {
    580             require_exact_discriminator_fields(object, &["type", "value"])?;
    581             Ok(RadrootsEventContractRegistryV7Discriminator::DTagExact {
    582                 value: discriminator_string_field(object, "value")?,
    583             })
    584         }
    585         "d_tag_prefix" => {
    586             require_exact_discriminator_fields(object, &["type", "prefix"])?;
    587             Ok(RadrootsEventContractRegistryV7Discriminator::DTagPrefix {
    588                 prefix: discriminator_string_field(object, "prefix")?,
    589             })
    590         }
    591         "d_tag_suffix" => {
    592             require_exact_discriminator_fields(object, &["type", "suffix"])?;
    593             Ok(RadrootsEventContractRegistryV7Discriminator::DTagSuffix {
    594                 suffix: discriminator_string_field(object, "suffix")?,
    595             })
    596         }
    597         "tag_equals" => {
    598             require_exact_discriminator_fields(object, &["type", "name", "value"])?;
    599             Ok(RadrootsEventContractRegistryV7Discriminator::TagEquals {
    600                 name: discriminator_string_field(object, "name")?,
    601                 value: discriminator_string_field(object, "value")?,
    602             })
    603         }
    604         "content_json_field_equals" => {
    605             require_exact_discriminator_fields(object, &["type", "field", "value"])?;
    606             Ok(
    607                 RadrootsEventContractRegistryV7Discriminator::ContentJsonFieldEquals {
    608                     field: discriminator_string_field(object, "field")?,
    609                     value: discriminator_string_field(object, "value")?,
    610                 },
    611             )
    612         }
    613         "envelope_type" => {
    614             require_exact_discriminator_fields(object, &["type", "value"])?;
    615             Ok(RadrootsEventContractRegistryV7Discriminator::EnvelopeType {
    616                 value: discriminator_string_field(object, "value")?,
    617             })
    618         }
    619         "composite" => {
    620             require_exact_discriminator_fields(object, &["type", "parts"])?;
    621             let parts = serde_json::from_value(
    622                 object
    623                     .get("parts")
    624                     .cloned()
    625                     .ok_or_else(|| "registry-v7 discriminator is missing parts".to_string())?,
    626             )
    627             .map_err(|error| format!("invalid registry-v7 composite discriminator: {error}"))?;
    628             Ok(RadrootsEventContractRegistryV7Discriminator::Composite { parts })
    629         }
    630         unknown => Err(format!(
    631             "unknown registry-v7 discriminator type {unknown:?}"
    632         )),
    633     }
    634 }
    635 
    636 fn require_exact_discriminator_fields(
    637     object: &serde_json::Map<String, serde_json::Value>,
    638     expected: &[&str],
    639 ) -> Result<(), String> {
    640     if object.len() != expected.len()
    641         || object
    642             .keys()
    643             .any(|field| !expected.contains(&field.as_str()))
    644     {
    645         return Err("registry-v7 discriminator contains unknown or missing fields".to_string());
    646     }
    647     Ok(())
    648 }
    649 
    650 fn discriminator_string_field(
    651     object: &serde_json::Map<String, serde_json::Value>,
    652     field: &str,
    653 ) -> Result<String, String> {
    654     object
    655         .get(field)
    656         .and_then(serde_json::Value::as_str)
    657         .map(ToString::to_string)
    658         .ok_or_else(|| format!("registry-v7 discriminator {field} must be a string"))
    659 }
    660 
    661 fn classified_listing_partition_inventory(
    662     value: ClassifiedListingPartition,
    663 ) -> RadrootsEventContractRegistryV7ClassifiedListingPartition {
    664     match value {
    665         ClassifiedListingPartition::FocusedFoodAvailability => {
    666             RadrootsEventContractRegistryV7ClassifiedListingPartition::FocusedFoodAvailability
    667         }
    668         ClassifiedListingPartition::OperationalListing => {
    669             RadrootsEventContractRegistryV7ClassifiedListingPartition::OperationalListing
    670         }
    671         ClassifiedListingPartition::GenericNip99 => {
    672             RadrootsEventContractRegistryV7ClassifiedListingPartition::GenericNip99
    673         }
    674         ClassifiedListingPartition::Ambiguous => {
    675             RadrootsEventContractRegistryV7ClassifiedListingPartition::Ambiguous
    676         }
    677     }
    678 }
    679 
    680 fn tag_inventory(contract: &TagContract) -> RadrootsEventContractRegistryV7Tag {
    681     RadrootsEventContractRegistryV7Tag {
    682         name: contract.name.to_string(),
    683         cardinality: tag_cardinality_inventory(contract.cardinality),
    684         semantic: tag_semantic_inventory(contract.semantic),
    685         value_type: tag_value_type_inventory(contract.value_type),
    686         relay_indexed: contract.relay_indexed,
    687     }
    688 }
    689 
    690 fn tag_cardinality_inventory(
    691     value: TagCardinality,
    692 ) -> RadrootsEventContractRegistryV7TagCardinality {
    693     match value {
    694         TagCardinality::RequiredOne => RadrootsEventContractRegistryV7TagCardinality::RequiredOne,
    695         TagCardinality::OptionalOne => RadrootsEventContractRegistryV7TagCardinality::OptionalOne,
    696         TagCardinality::OptionalMany => RadrootsEventContractRegistryV7TagCardinality::OptionalMany,
    697         TagCardinality::RequiredMany => RadrootsEventContractRegistryV7TagCardinality::RequiredMany,
    698     }
    699 }
    700 
    701 fn tag_semantic_inventory(value: TagSemantic) -> RadrootsEventContractRegistryV7TagSemantic {
    702     match value {
    703         TagSemantic::AddressableCoordinate => {
    704             RadrootsEventContractRegistryV7TagSemantic::AddressableCoordinate
    705         }
    706         TagSemantic::CalendarEventAuthor => {
    707             RadrootsEventContractRegistryV7TagSemantic::CalendarEventAuthor
    708         }
    709         TagSemantic::CalendarEventReference => {
    710             RadrootsEventContractRegistryV7TagSemantic::CalendarEventReference
    711         }
    712         TagSemantic::CalendarEventRevision => {
    713             RadrootsEventContractRegistryV7TagSemantic::CalendarEventRevision
    714         }
    715         TagSemantic::CalendarInclusionRequest => {
    716             RadrootsEventContractRegistryV7TagSemantic::CalendarInclusionRequest
    717         }
    718         TagSemantic::CalendarEnd => RadrootsEventContractRegistryV7TagSemantic::CalendarEnd,
    719         TagSemantic::CalendarStart => RadrootsEventContractRegistryV7TagSemantic::CalendarStart,
    720         TagSemantic::Category => RadrootsEventContractRegistryV7TagSemantic::Category,
    721         TagSemantic::Citation => RadrootsEventContractRegistryV7TagSemantic::Citation,
    722         TagSemantic::Contract => RadrootsEventContractRegistryV7TagSemantic::Contract,
    723         TagSemantic::Counterparty => RadrootsEventContractRegistryV7TagSemantic::Counterparty,
    724         TagSemantic::Evidence => RadrootsEventContractRegistryV7TagSemantic::Evidence,
    725         TagSemantic::EventPointer => RadrootsEventContractRegistryV7TagSemantic::EventPointer,
    726         TagSemantic::FreeBusy => RadrootsEventContractRegistryV7TagSemantic::FreeBusy,
    727         TagSemantic::Geohash => RadrootsEventContractRegistryV7TagSemantic::Geohash,
    728         TagSemantic::GroupId => RadrootsEventContractRegistryV7TagSemantic::GroupId,
    729         TagSemantic::Identifier => RadrootsEventContractRegistryV7TagSemantic::Identifier,
    730         TagSemantic::Image => RadrootsEventContractRegistryV7TagSemantic::Image,
    731         TagSemantic::Kind => RadrootsEventContractRegistryV7TagSemantic::Kind,
    732         TagSemantic::ClassifiedListingAddress => {
    733             RadrootsEventContractRegistryV7TagSemantic::ClassifiedListingAddress
    734         }
    735         TagSemantic::OperationalListingSnapshot => {
    736             RadrootsEventContractRegistryV7TagSemantic::OperationalListingSnapshot
    737         }
    738         TagSemantic::ListDescription => RadrootsEventContractRegistryV7TagSemantic::ListDescription,
    739         TagSemantic::Location => RadrootsEventContractRegistryV7TagSemantic::Location,
    740         TagSemantic::Nip01Coordinate => RadrootsEventContractRegistryV7TagSemantic::Nip01Coordinate,
    741         TagSemantic::Participant => RadrootsEventContractRegistryV7TagSemantic::Participant,
    742         TagSemantic::PreviousEvent => RadrootsEventContractRegistryV7TagSemantic::PreviousEvent,
    743         TagSemantic::Price => RadrootsEventContractRegistryV7TagSemantic::Price,
    744         TagSemantic::PublishedAt => RadrootsEventContractRegistryV7TagSemantic::PublishedAt,
    745         TagSemantic::Relay => RadrootsEventContractRegistryV7TagSemantic::Relay,
    746         TagSemantic::Reference => RadrootsEventContractRegistryV7TagSemantic::Reference,
    747         TagSemantic::ReviewTarget => RadrootsEventContractRegistryV7TagSemantic::ReviewTarget,
    748         TagSemantic::RootEvent => RadrootsEventContractRegistryV7TagSemantic::RootEvent,
    749         TagSemantic::ServiceInput => RadrootsEventContractRegistryV7TagSemantic::ServiceInput,
    750         TagSemantic::ServiceOutput => RadrootsEventContractRegistryV7TagSemantic::ServiceOutput,
    751         TagSemantic::Source => RadrootsEventContractRegistryV7TagSemantic::Source,
    752         TagSemantic::Status => RadrootsEventContractRegistryV7TagSemantic::Status,
    753         TagSemantic::Summary => RadrootsEventContractRegistryV7TagSemantic::Summary,
    754         TagSemantic::Title => RadrootsEventContractRegistryV7TagSemantic::Title,
    755         TagSemantic::Topic => RadrootsEventContractRegistryV7TagSemantic::Topic,
    756         TagSemantic::TimeZone => RadrootsEventContractRegistryV7TagSemantic::TimeZone,
    757         TagSemantic::TradeMutation => RadrootsEventContractRegistryV7TagSemantic::TradeMutation,
    758         TagSemantic::Url => RadrootsEventContractRegistryV7TagSemantic::Url,
    759         TagSemantic::UtcDayCoverage => RadrootsEventContractRegistryV7TagSemantic::UtcDayCoverage,
    760     }
    761 }
    762 
    763 fn tag_value_type_inventory(value: TagValueType) -> RadrootsEventContractRegistryV7TagValueType {
    764     match value {
    765         TagValueType::AddressableCoordinate => {
    766             RadrootsEventContractRegistryV7TagValueType::AddressableCoordinate
    767         }
    768         TagValueType::CalendarDate => RadrootsEventContractRegistryV7TagValueType::CalendarDate,
    769         TagValueType::CalendarEventCoordinate => {
    770             RadrootsEventContractRegistryV7TagValueType::CalendarEventCoordinate
    771         }
    772         TagValueType::CalendarFreeBusy => {
    773             RadrootsEventContractRegistryV7TagValueType::CalendarFreeBusy
    774         }
    775         TagValueType::CalendarRsvpStatus => {
    776             RadrootsEventContractRegistryV7TagValueType::CalendarRsvpStatus
    777         }
    778         TagValueType::CalendarUid => RadrootsEventContractRegistryV7TagValueType::CalendarUid,
    779         TagValueType::ContractId => RadrootsEventContractRegistryV7TagValueType::ContractId,
    780         TagValueType::DTag => RadrootsEventContractRegistryV7TagValueType::DTag,
    781         TagValueType::EventId => RadrootsEventContractRegistryV7TagValueType::EventId,
    782         TagValueType::EventPointer => RadrootsEventContractRegistryV7TagValueType::EventPointer,
    783         TagValueType::Geohash => RadrootsEventContractRegistryV7TagValueType::Geohash,
    784         TagValueType::IanaTimeZoneId => RadrootsEventContractRegistryV7TagValueType::IanaTimeZoneId,
    785         TagValueType::Kind => RadrootsEventContractRegistryV7TagValueType::Kind,
    786         TagValueType::MutationId => RadrootsEventContractRegistryV7TagValueType::MutationId,
    787         TagValueType::Nip01Coordinate => {
    788             RadrootsEventContractRegistryV7TagValueType::Nip01Coordinate
    789         }
    790         TagValueType::PublicKey => RadrootsEventContractRegistryV7TagValueType::PublicKey,
    791         TagValueType::RelayUrl => RadrootsEventContractRegistryV7TagValueType::RelayUrl,
    792         TagValueType::Sha256 => RadrootsEventContractRegistryV7TagValueType::Sha256,
    793         TagValueType::Text => RadrootsEventContractRegistryV7TagValueType::Text,
    794         TagValueType::UnixTimestamp => RadrootsEventContractRegistryV7TagValueType::UnixTimestamp,
    795         TagValueType::Uri => RadrootsEventContractRegistryV7TagValueType::Uri,
    796         TagValueType::Url => RadrootsEventContractRegistryV7TagValueType::Url,
    797         TagValueType::UtcDayIndex => RadrootsEventContractRegistryV7TagValueType::UtcDayIndex,
    798         TagValueType::Uuid => RadrootsEventContractRegistryV7TagValueType::Uuid,
    799     }
    800 }
    801 
    802 fn reducer_inventory(value: Reducer) -> RadrootsEventContractRegistryV7Reducer {
    803     match value {
    804         Reducer::CalendarProjection => RadrootsEventContractRegistryV7Reducer::CalendarProjection,
    805         Reducer::FarmOpsProjection => RadrootsEventContractRegistryV7Reducer::FarmOpsProjection,
    806         Reducer::GroupProjection => RadrootsEventContractRegistryV7Reducer::GroupProjection,
    807         Reducer::KnowledgeProjection => RadrootsEventContractRegistryV7Reducer::KnowledgeProjection,
    808         Reducer::OperationalListingInventoryAccounting => {
    809             RadrootsEventContractRegistryV7Reducer::OperationalListingInventoryAccounting
    810         }
    811         Reducer::OperationalListingProjection => {
    812             RadrootsEventContractRegistryV7Reducer::OperationalListingProjection
    813         }
    814         Reducer::MarketProjection => RadrootsEventContractRegistryV7Reducer::MarketProjection,
    815         Reducer::OrderProjection => RadrootsEventContractRegistryV7Reducer::OrderProjection,
    816         Reducer::ProfileProjection => RadrootsEventContractRegistryV7Reducer::ProfileProjection,
    817         Reducer::NostrRelayPolicyProjection => {
    818             RadrootsEventContractRegistryV7Reducer::NostrRelayPolicyProjection
    819         }
    820         Reducer::SocialProjection => RadrootsEventContractRegistryV7Reducer::SocialProjection,
    821         Reducer::TradeProjection => RadrootsEventContractRegistryV7Reducer::TradeProjection,
    822         Reducer::TradeValidation => RadrootsEventContractRegistryV7Reducer::TradeValidation,
    823     }
    824 }
    825 
    826 #[cfg(test)]
    827 mod tests {
    828     use super::*;
    829 
    830     #[test]
    831     fn inventory_pins_runtime_order_and_counts() {
    832         let inventory = event_contract_registry_v7_inventory();
    833 
    834         assert_eq!(
    835             inventory.schema_version,
    836             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_INVENTORY_SCHEMA_VERSION
    837         );
    838         assert_eq!(
    839             inventory.event_contract_registry_version,
    840             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_VERSION
    841         );
    842         assert_eq!(
    843             inventory.kind_contracts.len(),
    844             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_KIND_COUNT
    845         );
    846         assert_eq!(
    847             inventory.event_contracts.len(),
    848             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_EVENT_COUNT
    849         );
    850         assert!(
    851             inventory
    852                 .kind_contracts
    853                 .iter()
    854                 .enumerate()
    855                 .all(|(ordinal, contract)| contract.ordinal == ordinal)
    856         );
    857         assert!(
    858             inventory
    859                 .event_contracts
    860                 .iter()
    861                 .enumerate()
    862                 .all(|(ordinal, contract)| contract.ordinal == ordinal)
    863         );
    864     }
    865 
    866     #[test]
    867     fn inventory_json_is_canonical_and_round_trips_strictly() {
    868         let json = event_contract_registry_v7_inventory_json().expect("serialize inventory");
    869 
    870         assert!(json.ends_with('\n'));
    871         assert!(!json.ends_with("\n\n"));
    872         assert!(!json.contains("\r\n"));
    873         assert!(json.contains("\n  \"schema_version\": 1,"));
    874         assert_eq!(
    875             parse_event_contract_registry_v7_inventory_json(&json).expect("parse inventory"),
    876             event_contract_registry_v7_inventory()
    877         );
    878     }
    879 
    880     #[test]
    881     fn inventory_parser_rejects_unknown_fields_and_enum_values() {
    882         let mut top_level =
    883             serde_json::to_value(event_contract_registry_v7_inventory()).expect("inventory value");
    884         top_level
    885             .as_object_mut()
    886             .expect("inventory object")
    887             .insert("unknown".to_string(), serde_json::Value::Bool(true));
    888         assert!(
    889             serde_json::from_value::<RadrootsEventContractRegistryV7Inventory>(top_level).is_err()
    890         );
    891 
    892         let mut nested =
    893             serde_json::to_value(event_contract_registry_v7_inventory()).expect("inventory value");
    894         nested["event_contracts"][0]["discriminator"]["unknown"] = serde_json::Value::Bool(true);
    895         assert!(
    896             serde_json::from_value::<RadrootsEventContractRegistryV7Inventory>(nested).is_err()
    897         );
    898 
    899         let mut enum_value =
    900             serde_json::to_value(event_contract_registry_v7_inventory()).expect("inventory value");
    901         enum_value["kind_contracts"][0]["class"] =
    902             serde_json::Value::String("future_class".to_string());
    903         assert!(
    904             serde_json::from_value::<RadrootsEventContractRegistryV7Inventory>(enum_value).is_err()
    905         );
    906     }
    907 
    908     #[test]
    909     fn inventory_digest_is_deterministic_lowercase_hex() {
    910         let first = event_contract_registry_v7_inventory_sha256().expect("hash inventory");
    911         let second = event_contract_registry_v7_inventory_sha256().expect("hash inventory");
    912 
    913         assert_eq!(first, second);
    914         assert_eq!(first.len(), 64);
    915         assert!(
    916             first
    917                 .bytes()
    918                 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    919         );
    920     }
    921 }