diff --git a/Cargo.lock b/Cargo.lock index e3150db..71df227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9955,6 +9955,7 @@ dependencies = [ "mini-moka 0.11.0", "n0-future", "regex", + "regex-lite", "reqwest", "serde", "serde_html_form", diff --git a/crates/weaver-api/lexicons/app_bsky_ageassurance_begin.json b/crates/weaver-api/lexicons/app_bsky_ageassurance_begin.json new file mode 100644 index 0000000..0fe901f --- /dev/null +++ b/crates/weaver-api/lexicons/app_bsky_ageassurance_begin.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "app.bsky.ageassurance.begin", + "defs": { + "main": { + "type": "procedure", + "description": "Initiate Age Assurance for an account.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "email", + "language", + "countryCode" + ], + "properties": { + "countryCode": { + "type": "string", + "description": "An ISO 3166-1 alpha-2 code of the user's location." + }, + "email": { + "type": "string", + "description": "The user's email address to receive Age Assurance instructions." + }, + "language": { + "type": "string", + "description": "The user's preferred language for communication during the Age Assurance process." + }, + "regionCode": { + "type": "string", + "description": "An optional ISO 3166-2 code of the user's region or state within the country." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#state" + } + }, + "errors": [ + { + "name": "InvalidEmail" + }, + { + "name": "DidTooLong" + }, + { + "name": "InvalidInitiation" + }, + { + "name": "RegionNotSupported" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/app_bsky_ageassurance_defs.json b/crates/weaver-api/lexicons/app_bsky_ageassurance_defs.json new file mode 100644 index 0000000..cddd57c --- /dev/null +++ b/crates/weaver-api/lexicons/app_bsky_ageassurance_defs.json @@ -0,0 +1,305 @@ +{ + "lexicon": 1, + "id": "app.bsky.ageassurance.defs", + "defs": { + "access": { + "type": "string", + "description": "The access level granted based on Age Assurance data we've processed.", + "knownValues": [ + "unknown", + "none", + "safe", + "full" + ] + }, + "config": { + "type": "object", + "description": "", + "required": [ + "regions" + ], + "properties": { + "regions": { + "type": "array", + "description": "The per-region Age Assurance configuration.", + "items": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#configRegion" + } + } + } + }, + "configRegion": { + "type": "object", + "description": "The Age Assurance configuration for a specific region.", + "required": [ + "countryCode", + "rules" + ], + "properties": { + "countryCode": { + "type": "string", + "description": "The ISO 3166-1 alpha-2 country code this configuration applies to." + }, + "regionCode": { + "type": "string", + "description": "The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country." + }, + "rules": { + "type": "array", + "description": "The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item.", + "items": { + "type": "union", + "refs": [ + "#configRegionRuleDefault", + "#configRegionRuleIfDeclaredOverAge", + "#configRegionRuleIfDeclaredUnderAge", + "#configRegionRuleIfAssuredOverAge", + "#configRegionRuleIfAssuredUnderAge", + "#configRegionRuleIfAccountNewerThan", + "#configRegionRuleIfAccountOlderThan" + ] + } + } + } + }, + "configRegionRuleDefault": { + "type": "object", + "description": "Age Assurance rule that applies by default.", + "required": [ + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + } + } + }, + "configRegionRuleIfAccountNewerThan": { + "type": "object", + "description": "Age Assurance rule that applies if the account is equal-to or newer than a certain date.", + "required": [ + "date", + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, + "date": { + "type": "string", + "description": "The date threshold as a datetime string.", + "format": "datetime" + } + } + }, + "configRegionRuleIfAccountOlderThan": { + "type": "object", + "description": "Age Assurance rule that applies if the account is older than a certain date.", + "required": [ + "date", + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, + "date": { + "type": "string", + "description": "The date threshold as a datetime string.", + "format": "datetime" + } + } + }, + "configRegionRuleIfAssuredOverAge": { + "type": "object", + "description": "Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.", + "required": [ + "age", + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + } + } + }, + "configRegionRuleIfAssuredUnderAge": { + "type": "object", + "description": "Age Assurance rule that applies if the user has been assured to be under a certain age.", + "required": [ + "age", + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + } + } + }, + "configRegionRuleIfDeclaredOverAge": { + "type": "object", + "description": "Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.", + "required": [ + "age", + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + } + } + }, + "configRegionRuleIfDeclaredUnderAge": { + "type": "object", + "description": "Age Assurance rule that applies if the user has declared themselves under a certain age.", + "required": [ + "age", + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + } + } + }, + "event": { + "type": "object", + "description": "Object used to store Age Assurance data in stash.", + "required": [ + "createdAt", + "status", + "access", + "attemptId", + "countryCode" + ], + "properties": { + "access": { + "type": "string", + "description": "The access level granted based on Age Assurance data we've processed.", + "knownValues": [ + "unknown", + "none", + "safe", + "full" + ] + }, + "attemptId": { + "type": "string", + "description": "The unique identifier for this instance of the Age Assurance flow, in UUID format." + }, + "completeIp": { + "type": "string", + "description": "The IP address used when completing the Age Assurance flow." + }, + "completeUa": { + "type": "string", + "description": "The user agent used when completing the Age Assurance flow." + }, + "countryCode": { + "type": "string", + "description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow." + }, + "createdAt": { + "type": "string", + "description": "The date and time of this write operation.", + "format": "datetime" + }, + "email": { + "type": "string", + "description": "The email used for Age Assurance." + }, + "initIp": { + "type": "string", + "description": "The IP address used when initiating the Age Assurance flow." + }, + "initUa": { + "type": "string", + "description": "The user agent used when initiating the Age Assurance flow." + }, + "regionCode": { + "type": "string", + "description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow." + }, + "status": { + "type": "string", + "description": "The status of the Age Assurance process.", + "knownValues": [ + "unknown", + "pending", + "assured", + "blocked" + ] + } + } + }, + "state": { + "type": "object", + "description": "The user's computed Age Assurance state.", + "required": [ + "status", + "access" + ], + "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, + "lastInitiatedAt": { + "type": "string", + "description": "The timestamp when this state was last updated.", + "format": "datetime" + }, + "status": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#status" + } + } + }, + "stateMetadata": { + "type": "object", + "description": "Additional metadata needed to compute Age Assurance state client-side.", + "required": [], + "properties": { + "accountCreatedAt": { + "type": "string", + "description": "The account creation timestamp.", + "format": "datetime" + } + } + }, + "status": { + "type": "string", + "description": "The status of the Age Assurance process.", + "knownValues": [ + "unknown", + "pending", + "assured", + "blocked" + ] + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/app_bsky_ageassurance_getConfig.json b/crates/weaver-api/lexicons/app_bsky_ageassurance_getConfig.json new file mode 100644 index 0000000..1e11577 --- /dev/null +++ b/crates/weaver-api/lexicons/app_bsky_ageassurance_getConfig.json @@ -0,0 +1,17 @@ +{ + "lexicon": 1, + "id": "app.bsky.ageassurance.getConfig", + "defs": { + "main": { + "type": "query", + "description": "Returns Age Assurance configuration for use on the client.", + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#config" + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/app_bsky_ageassurance_getState.json b/crates/weaver-api/lexicons/app_bsky_ageassurance_getState.json new file mode 100644 index 0000000..71ca4e6 --- /dev/null +++ b/crates/weaver-api/lexicons/app_bsky_ageassurance_getState.json @@ -0,0 +1,44 @@ +{ + "lexicon": 1, + "id": "app.bsky.ageassurance.getState", + "defs": { + "main": { + "type": "query", + "description": "Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.", + "parameters": { + "type": "params", + "required": [ + "countryCode" + ], + "properties": { + "countryCode": { + "type": "string" + }, + "regionCode": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "state", + "metadata" + ], + "properties": { + "metadata": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#stateMetadata" + }, + "state": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#state" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_edit_defs.json b/crates/weaver-api/lexicons/sh_weaver_edit_defs.json index 81f8c7b..471d5c7 100644 --- a/crates/weaver-api/lexicons/sh_weaver_edit_defs.json +++ b/crates/weaver-api/lexicons/sh_weaver_edit_defs.json @@ -12,18 +12,31 @@ "type": "union", "refs": [ "#notebookRef", - "#entryRef" + "#entryRef", + "#draftRef" ] } } }, + "draftRef": { + "type": "object", + "required": [ + "draft_key" + ], + "properties": { + "draft_key": { + "type": "string", + "maxLength": 200 + } + } + }, "entryRef": { "type": "object", "required": [ - "notebook" + "entry" ], "properties": { - "notebook": { + "entry": { "type": "ref", "ref": "com.atproto.repo.strongRef" } diff --git a/crates/weaver-api/lexicons/sh_weaver_edit_diff.json b/crates/weaver-api/lexicons/sh_weaver_edit_diff.json index ba32bac..8b3696c 100644 --- a/crates/weaver-api/lexicons/sh_weaver_edit_diff.json +++ b/crates/weaver-api/lexicons/sh_weaver_edit_diff.json @@ -18,6 +18,10 @@ "type": "ref", "ref": "sh.weaver.edit.defs#docRef" }, + "prev": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, "root": { "type": "ref", "ref": "com.atproto.repo.strongRef" diff --git a/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json b/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json index a414b7d..b85dcd4 100644 --- a/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json +++ b/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json @@ -131,6 +131,10 @@ "attemptId" ], "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, "attemptId": { "type": "string", "description": "The unique identifier for this instance of the age assurance flow, in UUID format." @@ -143,6 +147,10 @@ "type": "string", "description": "The user agent used when completing the AA flow." }, + "countryCode": { + "type": "string", + "description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow." + }, "createdAt": { "type": "string", "description": "The date and time of this write operation.", @@ -156,9 +164,13 @@ "type": "string", "description": "The user agent used when initiating the AA flow." }, + "regionCode": { + "type": "string", + "description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow." + }, "status": { "type": "string", - "description": "The status of the age assurance process.", + "description": "The status of the Age Assurance process.", "knownValues": [ "unknown", "pending", @@ -175,6 +187,10 @@ "status" ], "properties": { + "access": { + "type": "ref", + "ref": "app.bsky.ageassurance.defs#access" + }, "comment": { "type": "string", "description": "Comment describing the reason for the override." @@ -1321,10 +1337,10 @@ "subjectReviewState": { "type": "string", "knownValues": [ - "#reviewOpen", - "#reviewEscalated", - "#reviewClosed", - "#reviewNone" + "tools.ozone.moderation.defs#reviewOpen", + "tools.ozone.moderation.defs#reviewEscalated", + "tools.ozone.moderation.defs#reviewClosed", + "tools.ozone.moderation.defs#reviewNone" ] }, "subjectStatusView": { diff --git a/crates/weaver-api/lexicons/tools_ozone_team_defs.json b/crates/weaver-api/lexicons/tools_ozone_team_defs.json index 36982c0..93814a5 100644 --- a/crates/weaver-api/lexicons/tools_ozone_team_defs.json +++ b/crates/weaver-api/lexicons/tools_ozone_team_defs.json @@ -30,10 +30,10 @@ "role": { "type": "string", "knownValues": [ - "#roleAdmin", - "#roleModerator", - "#roleTriage", - "#roleVerifier" + "tools.ozone.team.defs#roleAdmin", + "tools.ozone.team.defs#roleModerator", + "tools.ozone.team.defs#roleTriage", + "tools.ozone.team.defs#roleVerifier" ] }, "updatedAt": { diff --git a/crates/weaver-api/src/app_bsky.rs b/crates/weaver-api/src/app_bsky.rs index 6cdcc8f..bd97c81 100644 --- a/crates/weaver-api/src/app_bsky.rs +++ b/crates/weaver-api/src/app_bsky.rs @@ -4,6 +4,7 @@ // Any manual changes will be overwritten on the next regeneration. pub mod actor; +pub mod ageassurance; pub mod bookmark; pub mod embed; pub mod feed; diff --git a/crates/weaver-api/src/app_bsky/ageassurance.rs b/crates/weaver-api/src/app_bsky/ageassurance.rs new file mode 100644 index 0000000..48019c5 --- /dev/null +++ b/crates/weaver-api/src/app_bsky/ageassurance.rs @@ -0,0 +1,3424 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: app.bsky.ageassurance.defs +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +pub mod begin; +pub mod get_config; +pub mod get_state; + +/// The access level granted based on Age Assurance data we've processed. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Access<'a> { + Unknown, + None, + Safe, + Full, + Other(jacquard_common::CowStr<'a>), +} + +impl<'a> Access<'a> { + pub fn as_str(&self) -> &str { + match self { + Self::Unknown => "unknown", + Self::None => "none", + Self::Safe => "safe", + Self::Full => "full", + Self::Other(s) => s.as_ref(), + } + } +} + +impl<'a> From<&'a str> for Access<'a> { + fn from(s: &'a str) -> Self { + match s { + "unknown" => Self::Unknown, + "none" => Self::None, + "safe" => Self::Safe, + "full" => Self::Full, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> From for Access<'a> { + fn from(s: String) -> Self { + match s.as_str() { + "unknown" => Self::Unknown, + "none" => Self::None, + "safe" => Self::Safe, + "full" => Self::Full, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> AsRef for Access<'a> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl<'a> serde::Serialize for Access<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de, 'a> serde::Deserialize<'de> for Access<'a> +where + 'de: 'a, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = <&'de str>::deserialize(deserializer)?; + Ok(Self::from(s)) + } +} + +impl jacquard_common::IntoStatic for Access<'_> { + type Output = Access<'static>; + fn into_static(self) -> Self::Output { + match self { + Access::Unknown => Access::Unknown, + Access::None => Access::None, + Access::Safe => Access::Safe, + Access::Full => Access::Full, + Access::Other(v) => Access::Other(v.into_static()), + } + } +} + +/// +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Config<'a> { + /// The per-region Age Assurance configuration. + #[serde(borrow)] + pub regions: Vec>, +} + +pub mod config_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Regions; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Regions = Unset; + } + ///State transition - sets the `regions` field to Set + pub struct SetRegions(PhantomData S>); + impl sealed::Sealed for SetRegions {} + impl State for SetRegions { + type Regions = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `regions` field + pub struct regions(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigBuilder<'a, S: config_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> Config<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigBuilder<'a, config_state::Empty> { + ConfigBuilder::new() + } +} + +impl<'a> ConfigBuilder<'a, config_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None,), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigBuilder<'a, S> +where + S: config_state::State, + S::Regions: config_state::IsUnset, +{ + /// Set the `regions` field (required) + pub fn regions( + mut self, + value: impl Into>>, + ) -> ConfigBuilder<'a, config_state::SetRegions> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigBuilder<'a, S> +where + S: config_state::State, + S::Regions: config_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Config<'a> { + Config { + regions: self.__unsafe_private_named.0.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> Config<'a> { + Config { + regions: self.__unsafe_private_named.0.unwrap(), + extra_data: Some(extra_data), + } + } +} + +fn lexicon_doc_app_bsky_ageassurance_defs() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("app.bsky.ageassurance.defs"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexUserType::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The access level granted based on Age Assurance data we've processed.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("config"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some(::jacquard_common::CowStr::new_static("")), + required: Some( + vec![::jacquard_common::smol_str::SmolStr::new_static("regions")], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("regions"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { + description: Some( + ::jacquard_common::CowStr::new_static( + "The per-region Age Assurance configuration.", + ), + ), + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#configRegion", + ), + }), + min_length: None, + max_length: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("configRegion"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "The Age Assurance configuration for a specific region.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("countryCode"), + ::jacquard_common::smol_str::SmolStr::new_static("rules") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "countryCode", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The ISO 3166-1 alpha-2 country code this configuration applies to.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "regionCode", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("rules"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { + description: Some( + ::jacquard_common::CowStr::new_static( + "The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item.", + ), + ), + items: ::jacquard_lexicon::lexicon::LexArrayItem::Union(::jacquard_lexicon::lexicon::LexRefUnion { + description: None, + refs: vec![ + ::jacquard_common::CowStr::new_static("#configRegionRuleDefault"), + ::jacquard_common::CowStr::new_static("#configRegionRuleIfDeclaredOverAge"), + ::jacquard_common::CowStr::new_static("#configRegionRuleIfDeclaredUnderAge"), + ::jacquard_common::CowStr::new_static("#configRegionRuleIfAssuredOverAge"), + ::jacquard_common::CowStr::new_static("#configRegionRuleIfAssuredUnderAge"), + ::jacquard_common::CowStr::new_static("#configRegionRuleIfAccountNewerThan"), + ::jacquard_common::CowStr::new_static("#configRegionRuleIfAccountOlderThan") + ], + closed: None, + }), + min_length: None, + max_length: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "configRegionRuleDefault", + ), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Age Assurance rule that applies by default.", + ), + ), + required: Some( + vec![::jacquard_common::smol_str::SmolStr::new_static("access")], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "configRegionRuleIfAccountNewerThan", + ), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Age Assurance rule that applies if the account is equal-to or newer than a certain date.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("date"), + ::jacquard_common::smol_str::SmolStr::new_static("access") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("date"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The date threshold as a datetime string.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, + ), + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "configRegionRuleIfAccountOlderThan", + ), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Age Assurance rule that applies if the account is older than a certain date.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("date"), + ::jacquard_common::smol_str::SmolStr::new_static("access") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("date"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The date threshold as a datetime string.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, + ), + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "configRegionRuleIfAssuredOverAge", + ), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_common::smol_str::SmolStr::new_static("access") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { + description: None, + default: None, + minimum: None, + maximum: None, + r#enum: None, + r#const: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "configRegionRuleIfAssuredUnderAge", + ), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Age Assurance rule that applies if the user has been assured to be under a certain age.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_common::smol_str::SmolStr::new_static("access") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { + description: None, + default: None, + minimum: None, + maximum: None, + r#enum: None, + r#const: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "configRegionRuleIfDeclaredOverAge", + ), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_common::smol_str::SmolStr::new_static("access") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { + description: None, + default: None, + minimum: None, + maximum: None, + r#enum: None, + r#const: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "configRegionRuleIfDeclaredUnderAge", + ), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Age Assurance rule that applies if the user has declared themselves under a certain age.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_common::smol_str::SmolStr::new_static("access") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("age"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { + description: None, + default: None, + minimum: None, + maximum: None, + r#enum: None, + r#const: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("event"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Object used to store Age Assurance data in stash.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("createdAt"), + ::jacquard_common::smol_str::SmolStr::new_static("status"), + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_common::smol_str::SmolStr::new_static("attemptId"), + ::jacquard_common::smol_str::SmolStr::new_static("countryCode") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The access level granted based on Age Assurance data we've processed.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "attemptId", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The unique identifier for this instance of the Age Assurance flow, in UUID format.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "completeIp", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The IP address used when completing the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "completeUa", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The user agent used when completing the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "countryCode", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The date and time of this write operation.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, + ), + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("email"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The email used for Age Assurance.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("initIp"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The IP address used when initiating the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("initUa"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The user agent used when initiating the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "regionCode", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The ISO 3166-2 region code provided when beginning the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("status"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The status of the Age Assurance process.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("state"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "The user's computed Age Assurance state.", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("status"), + ::jacquard_common::smol_str::SmolStr::new_static("access") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "lastInitiatedAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The timestamp when this state was last updated.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, + ), + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("status"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#status", + ), + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("stateMetadata"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Additional metadata needed to compute Age Assurance state client-side.", + ), + ), + required: Some(vec![]), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "accountCreatedAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The account creation timestamp.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, + ), + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("status"), + ::jacquard_lexicon::lexicon::LexUserType::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The status of the Age Assurance process.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Config<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "config" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// The Age Assurance configuration for a specific region. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegion<'a> { + /// The ISO 3166-1 alpha-2 country code this configuration applies to. + #[serde(borrow)] + pub country_code: jacquard_common::CowStr<'a>, + /// The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub region_code: std::option::Option>, + /// The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item. + #[serde(borrow)] + pub rules: Vec>, +} + +pub mod config_region_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type CountryCode; + type Rules; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type CountryCode = Unset; + type Rules = Unset; + } + ///State transition - sets the `country_code` field to Set + pub struct SetCountryCode(PhantomData S>); + impl sealed::Sealed for SetCountryCode {} + impl State for SetCountryCode { + type CountryCode = Set; + type Rules = S::Rules; + } + ///State transition - sets the `rules` field to Set + pub struct SetRules(PhantomData S>); + impl sealed::Sealed for SetRules {} + impl State for SetRules { + type CountryCode = S::CountryCode; + type Rules = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `country_code` field + pub struct country_code(()); + ///Marker type for the `rules` field + pub struct rules(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionBuilder<'a, S: config_region_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegion<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionBuilder<'a, config_region_state::Empty> { + ConfigRegionBuilder::new() + } +} + +impl<'a> ConfigRegionBuilder<'a, config_region_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionBuilder<'a, S> +where + S: config_region_state::State, + S::CountryCode: config_region_state::IsUnset, +{ + /// Set the `countryCode` field (required) + pub fn country_code( + mut self, + value: impl Into>, + ) -> ConfigRegionBuilder<'a, config_region_state::SetCountryCode> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: config_region_state::State> ConfigRegionBuilder<'a, S> { + /// Set the `regionCode` field (optional) + pub fn region_code( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.1 = value.into(); + self + } + /// Set the `regionCode` field to an Option value (optional) + pub fn maybe_region_code( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.1 = value; + self + } +} + +impl<'a, S> ConfigRegionBuilder<'a, S> +where + S: config_region_state::State, + S::Rules: config_region_state::IsUnset, +{ + /// Set the `rules` field (required) + pub fn rules( + mut self, + value: impl Into>>, + ) -> ConfigRegionBuilder<'a, config_region_state::SetRules> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + ConfigRegionBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionBuilder<'a, S> +where + S: config_region_state::State, + S::CountryCode: config_region_state::IsSet, + S::Rules: config_region_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegion<'a> { + ConfigRegion { + country_code: self.__unsafe_private_named.0.unwrap(), + region_code: self.__unsafe_private_named.1, + rules: self.__unsafe_private_named.2.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegion<'a> { + ConfigRegion { + country_code: self.__unsafe_private_named.0.unwrap(), + region_code: self.__unsafe_private_named.1, + rules: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } +} + +#[jacquard_derive::open_union] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(tag = "$type")] +#[serde(bound(deserialize = "'de: 'a"))] +pub enum ConfigRegionRulesItem<'a> { + #[serde(rename = "app.bsky.ageassurance.defs#configRegionRuleDefault")] + ConfigRegionRuleDefault( + Box>, + ), + #[serde(rename = "app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge")] + ConfigRegionRuleIfDeclaredOverAge( + Box>, + ), + #[serde(rename = "app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge")] + ConfigRegionRuleIfDeclaredUnderAge( + Box>, + ), + #[serde(rename = "app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge")] + ConfigRegionRuleIfAssuredOverAge( + Box>, + ), + #[serde(rename = "app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge")] + ConfigRegionRuleIfAssuredUnderAge( + Box>, + ), + #[serde(rename = "app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan")] + ConfigRegionRuleIfAccountNewerThan( + Box>, + ), + #[serde(rename = "app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan")] + ConfigRegionRuleIfAccountOlderThan( + Box>, + ), +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ConfigRegion<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegion" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Age Assurance rule that applies by default. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegionRuleDefault<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, +} + +pub mod config_region_rule_default_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Access = Unset; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionRuleDefaultBuilder< + 'a, + S: config_region_rule_default_state::State, +> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegionRuleDefault<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionRuleDefaultBuilder< + 'a, + config_region_rule_default_state::Empty, + > { + ConfigRegionRuleDefaultBuilder::new() + } +} + +impl<'a> ConfigRegionRuleDefaultBuilder<'a, config_region_rule_default_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionRuleDefaultBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None,), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleDefaultBuilder<'a, S> +where + S: config_region_rule_default_state::State, + S::Access: config_region_rule_default_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> ConfigRegionRuleDefaultBuilder< + 'a, + config_region_rule_default_state::SetAccess, + > { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleDefaultBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleDefaultBuilder<'a, S> +where + S: config_region_rule_default_state::State, + S::Access: config_region_rule_default_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegionRuleDefault<'a> { + ConfigRegionRuleDefault { + access: self.__unsafe_private_named.0.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegionRuleDefault<'a> { + ConfigRegionRuleDefault { + access: self.__unsafe_private_named.0.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ConfigRegionRuleDefault<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegionRuleDefault" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Age Assurance rule that applies if the account is equal-to or newer than a certain date. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegionRuleIfAccountNewerThan<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, + /// The date threshold as a datetime string. + pub date: jacquard_common::types::string::Datetime, +} + +pub mod config_region_rule_if_account_newer_than_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Date; + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Date = Unset; + type Access = Unset; + } + ///State transition - sets the `date` field to Set + pub struct SetDate(PhantomData S>); + impl sealed::Sealed for SetDate {} + impl State for SetDate { + type Date = Set; + type Access = S::Access; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Date = S::Date; + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `date` field + pub struct date(()); + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionRuleIfAccountNewerThanBuilder< + 'a, + S: config_region_rule_if_account_newer_than_state::State, +> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegionRuleIfAccountNewerThan<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionRuleIfAccountNewerThanBuilder< + 'a, + config_region_rule_if_account_newer_than_state::Empty, + > { + ConfigRegionRuleIfAccountNewerThanBuilder::new() + } +} + +impl< + 'a, +> ConfigRegionRuleIfAccountNewerThanBuilder< + 'a, + config_region_rule_if_account_newer_than_state::Empty, +> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionRuleIfAccountNewerThanBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAccountNewerThanBuilder<'a, S> +where + S: config_region_rule_if_account_newer_than_state::State, + S::Access: config_region_rule_if_account_newer_than_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> ConfigRegionRuleIfAccountNewerThanBuilder< + 'a, + config_region_rule_if_account_newer_than_state::SetAccess, + > { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAccountNewerThanBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAccountNewerThanBuilder<'a, S> +where + S: config_region_rule_if_account_newer_than_state::State, + S::Date: config_region_rule_if_account_newer_than_state::IsUnset, +{ + /// Set the `date` field (required) + pub fn date( + mut self, + value: impl Into, + ) -> ConfigRegionRuleIfAccountNewerThanBuilder< + 'a, + config_region_rule_if_account_newer_than_state::SetDate, + > { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAccountNewerThanBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAccountNewerThanBuilder<'a, S> +where + S: config_region_rule_if_account_newer_than_state::State, + S::Date: config_region_rule_if_account_newer_than_state::IsSet, + S::Access: config_region_rule_if_account_newer_than_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegionRuleIfAccountNewerThan<'a> { + ConfigRegionRuleIfAccountNewerThan { + access: self.__unsafe_private_named.0.unwrap(), + date: self.__unsafe_private_named.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegionRuleIfAccountNewerThan<'a> { + ConfigRegionRuleIfAccountNewerThan { + access: self.__unsafe_private_named.0.unwrap(), + date: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema +for ConfigRegionRuleIfAccountNewerThan<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegionRuleIfAccountNewerThan" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Age Assurance rule that applies if the account is older than a certain date. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegionRuleIfAccountOlderThan<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, + /// The date threshold as a datetime string. + pub date: jacquard_common::types::string::Datetime, +} + +pub mod config_region_rule_if_account_older_than_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Date; + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Date = Unset; + type Access = Unset; + } + ///State transition - sets the `date` field to Set + pub struct SetDate(PhantomData S>); + impl sealed::Sealed for SetDate {} + impl State for SetDate { + type Date = Set; + type Access = S::Access; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Date = S::Date; + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `date` field + pub struct date(()); + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionRuleIfAccountOlderThanBuilder< + 'a, + S: config_region_rule_if_account_older_than_state::State, +> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegionRuleIfAccountOlderThan<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionRuleIfAccountOlderThanBuilder< + 'a, + config_region_rule_if_account_older_than_state::Empty, + > { + ConfigRegionRuleIfAccountOlderThanBuilder::new() + } +} + +impl< + 'a, +> ConfigRegionRuleIfAccountOlderThanBuilder< + 'a, + config_region_rule_if_account_older_than_state::Empty, +> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionRuleIfAccountOlderThanBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAccountOlderThanBuilder<'a, S> +where + S: config_region_rule_if_account_older_than_state::State, + S::Access: config_region_rule_if_account_older_than_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> ConfigRegionRuleIfAccountOlderThanBuilder< + 'a, + config_region_rule_if_account_older_than_state::SetAccess, + > { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAccountOlderThanBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAccountOlderThanBuilder<'a, S> +where + S: config_region_rule_if_account_older_than_state::State, + S::Date: config_region_rule_if_account_older_than_state::IsUnset, +{ + /// Set the `date` field (required) + pub fn date( + mut self, + value: impl Into, + ) -> ConfigRegionRuleIfAccountOlderThanBuilder< + 'a, + config_region_rule_if_account_older_than_state::SetDate, + > { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAccountOlderThanBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAccountOlderThanBuilder<'a, S> +where + S: config_region_rule_if_account_older_than_state::State, + S::Date: config_region_rule_if_account_older_than_state::IsSet, + S::Access: config_region_rule_if_account_older_than_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegionRuleIfAccountOlderThan<'a> { + ConfigRegionRuleIfAccountOlderThan { + access: self.__unsafe_private_named.0.unwrap(), + date: self.__unsafe_private_named.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegionRuleIfAccountOlderThan<'a> { + ConfigRegionRuleIfAccountOlderThan { + access: self.__unsafe_private_named.0.unwrap(), + date: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema +for ConfigRegionRuleIfAccountOlderThan<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegionRuleIfAccountOlderThan" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegionRuleIfAssuredOverAge<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, + /// The age threshold as a whole integer. + pub age: i64, +} + +pub mod config_region_rule_if_assured_over_age_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Age; + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Age = Unset; + type Access = Unset; + } + ///State transition - sets the `age` field to Set + pub struct SetAge(PhantomData S>); + impl sealed::Sealed for SetAge {} + impl State for SetAge { + type Age = Set; + type Access = S::Access; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Age = S::Age; + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `age` field + pub struct age(()); + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionRuleIfAssuredOverAgeBuilder< + 'a, + S: config_region_rule_if_assured_over_age_state::State, +> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegionRuleIfAssuredOverAge<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionRuleIfAssuredOverAgeBuilder< + 'a, + config_region_rule_if_assured_over_age_state::Empty, + > { + ConfigRegionRuleIfAssuredOverAgeBuilder::new() + } +} + +impl< + 'a, +> ConfigRegionRuleIfAssuredOverAgeBuilder< + 'a, + config_region_rule_if_assured_over_age_state::Empty, +> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionRuleIfAssuredOverAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAssuredOverAgeBuilder<'a, S> +where + S: config_region_rule_if_assured_over_age_state::State, + S::Access: config_region_rule_if_assured_over_age_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> ConfigRegionRuleIfAssuredOverAgeBuilder< + 'a, + config_region_rule_if_assured_over_age_state::SetAccess, + > { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAssuredOverAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAssuredOverAgeBuilder<'a, S> +where + S: config_region_rule_if_assured_over_age_state::State, + S::Age: config_region_rule_if_assured_over_age_state::IsUnset, +{ + /// Set the `age` field (required) + pub fn age( + mut self, + value: impl Into, + ) -> ConfigRegionRuleIfAssuredOverAgeBuilder< + 'a, + config_region_rule_if_assured_over_age_state::SetAge, + > { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAssuredOverAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAssuredOverAgeBuilder<'a, S> +where + S: config_region_rule_if_assured_over_age_state::State, + S::Age: config_region_rule_if_assured_over_age_state::IsSet, + S::Access: config_region_rule_if_assured_over_age_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegionRuleIfAssuredOverAge<'a> { + ConfigRegionRuleIfAssuredOverAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegionRuleIfAssuredOverAge<'a> { + ConfigRegionRuleIfAssuredOverAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema +for ConfigRegionRuleIfAssuredOverAge<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegionRuleIfAssuredOverAge" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Age Assurance rule that applies if the user has been assured to be under a certain age. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegionRuleIfAssuredUnderAge<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, + /// The age threshold as a whole integer. + pub age: i64, +} + +pub mod config_region_rule_if_assured_under_age_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Age; + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Age = Unset; + type Access = Unset; + } + ///State transition - sets the `age` field to Set + pub struct SetAge(PhantomData S>); + impl sealed::Sealed for SetAge {} + impl State for SetAge { + type Age = Set; + type Access = S::Access; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Age = S::Age; + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `age` field + pub struct age(()); + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionRuleIfAssuredUnderAgeBuilder< + 'a, + S: config_region_rule_if_assured_under_age_state::State, +> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegionRuleIfAssuredUnderAge<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionRuleIfAssuredUnderAgeBuilder< + 'a, + config_region_rule_if_assured_under_age_state::Empty, + > { + ConfigRegionRuleIfAssuredUnderAgeBuilder::new() + } +} + +impl< + 'a, +> ConfigRegionRuleIfAssuredUnderAgeBuilder< + 'a, + config_region_rule_if_assured_under_age_state::Empty, +> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionRuleIfAssuredUnderAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAssuredUnderAgeBuilder<'a, S> +where + S: config_region_rule_if_assured_under_age_state::State, + S::Access: config_region_rule_if_assured_under_age_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> ConfigRegionRuleIfAssuredUnderAgeBuilder< + 'a, + config_region_rule_if_assured_under_age_state::SetAccess, + > { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAssuredUnderAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAssuredUnderAgeBuilder<'a, S> +where + S: config_region_rule_if_assured_under_age_state::State, + S::Age: config_region_rule_if_assured_under_age_state::IsUnset, +{ + /// Set the `age` field (required) + pub fn age( + mut self, + value: impl Into, + ) -> ConfigRegionRuleIfAssuredUnderAgeBuilder< + 'a, + config_region_rule_if_assured_under_age_state::SetAge, + > { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfAssuredUnderAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfAssuredUnderAgeBuilder<'a, S> +where + S: config_region_rule_if_assured_under_age_state::State, + S::Age: config_region_rule_if_assured_under_age_state::IsSet, + S::Access: config_region_rule_if_assured_under_age_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegionRuleIfAssuredUnderAge<'a> { + ConfigRegionRuleIfAssuredUnderAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegionRuleIfAssuredUnderAge<'a> { + ConfigRegionRuleIfAssuredUnderAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema +for ConfigRegionRuleIfAssuredUnderAge<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegionRuleIfAssuredUnderAge" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegionRuleIfDeclaredOverAge<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, + /// The age threshold as a whole integer. + pub age: i64, +} + +pub mod config_region_rule_if_declared_over_age_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Age; + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Age = Unset; + type Access = Unset; + } + ///State transition - sets the `age` field to Set + pub struct SetAge(PhantomData S>); + impl sealed::Sealed for SetAge {} + impl State for SetAge { + type Age = Set; + type Access = S::Access; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Age = S::Age; + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `age` field + pub struct age(()); + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionRuleIfDeclaredOverAgeBuilder< + 'a, + S: config_region_rule_if_declared_over_age_state::State, +> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegionRuleIfDeclaredOverAge<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionRuleIfDeclaredOverAgeBuilder< + 'a, + config_region_rule_if_declared_over_age_state::Empty, + > { + ConfigRegionRuleIfDeclaredOverAgeBuilder::new() + } +} + +impl< + 'a, +> ConfigRegionRuleIfDeclaredOverAgeBuilder< + 'a, + config_region_rule_if_declared_over_age_state::Empty, +> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionRuleIfDeclaredOverAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfDeclaredOverAgeBuilder<'a, S> +where + S: config_region_rule_if_declared_over_age_state::State, + S::Access: config_region_rule_if_declared_over_age_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> ConfigRegionRuleIfDeclaredOverAgeBuilder< + 'a, + config_region_rule_if_declared_over_age_state::SetAccess, + > { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfDeclaredOverAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfDeclaredOverAgeBuilder<'a, S> +where + S: config_region_rule_if_declared_over_age_state::State, + S::Age: config_region_rule_if_declared_over_age_state::IsUnset, +{ + /// Set the `age` field (required) + pub fn age( + mut self, + value: impl Into, + ) -> ConfigRegionRuleIfDeclaredOverAgeBuilder< + 'a, + config_region_rule_if_declared_over_age_state::SetAge, + > { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfDeclaredOverAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfDeclaredOverAgeBuilder<'a, S> +where + S: config_region_rule_if_declared_over_age_state::State, + S::Age: config_region_rule_if_declared_over_age_state::IsSet, + S::Access: config_region_rule_if_declared_over_age_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegionRuleIfDeclaredOverAge<'a> { + ConfigRegionRuleIfDeclaredOverAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegionRuleIfDeclaredOverAge<'a> { + ConfigRegionRuleIfDeclaredOverAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema +for ConfigRegionRuleIfDeclaredOverAge<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegionRuleIfDeclaredOverAge" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Age Assurance rule that applies if the user has declared themselves under a certain age. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ConfigRegionRuleIfDeclaredUnderAge<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, + /// The age threshold as a whole integer. + pub age: i64, +} + +pub mod config_region_rule_if_declared_under_age_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Age; + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Age = Unset; + type Access = Unset; + } + ///State transition - sets the `age` field to Set + pub struct SetAge(PhantomData S>); + impl sealed::Sealed for SetAge {} + impl State for SetAge { + type Age = Set; + type Access = S::Access; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Age = S::Age; + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `age` field + pub struct age(()); + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ConfigRegionRuleIfDeclaredUnderAgeBuilder< + 'a, + S: config_region_rule_if_declared_under_age_state::State, +> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ConfigRegionRuleIfDeclaredUnderAge<'a> { + /// Create a new builder for this type + pub fn new() -> ConfigRegionRuleIfDeclaredUnderAgeBuilder< + 'a, + config_region_rule_if_declared_under_age_state::Empty, + > { + ConfigRegionRuleIfDeclaredUnderAgeBuilder::new() + } +} + +impl< + 'a, +> ConfigRegionRuleIfDeclaredUnderAgeBuilder< + 'a, + config_region_rule_if_declared_under_age_state::Empty, +> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ConfigRegionRuleIfDeclaredUnderAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfDeclaredUnderAgeBuilder<'a, S> +where + S: config_region_rule_if_declared_under_age_state::State, + S::Access: config_region_rule_if_declared_under_age_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> ConfigRegionRuleIfDeclaredUnderAgeBuilder< + 'a, + config_region_rule_if_declared_under_age_state::SetAccess, + > { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfDeclaredUnderAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfDeclaredUnderAgeBuilder<'a, S> +where + S: config_region_rule_if_declared_under_age_state::State, + S::Age: config_region_rule_if_declared_under_age_state::IsUnset, +{ + /// Set the `age` field (required) + pub fn age( + mut self, + value: impl Into, + ) -> ConfigRegionRuleIfDeclaredUnderAgeBuilder< + 'a, + config_region_rule_if_declared_under_age_state::SetAge, + > { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ConfigRegionRuleIfDeclaredUnderAgeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ConfigRegionRuleIfDeclaredUnderAgeBuilder<'a, S> +where + S: config_region_rule_if_declared_under_age_state::State, + S::Age: config_region_rule_if_declared_under_age_state::IsSet, + S::Access: config_region_rule_if_declared_under_age_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ConfigRegionRuleIfDeclaredUnderAge<'a> { + ConfigRegionRuleIfDeclaredUnderAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ConfigRegionRuleIfDeclaredUnderAge<'a> { + ConfigRegionRuleIfDeclaredUnderAge { + access: self.__unsafe_private_named.0.unwrap(), + age: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema +for ConfigRegionRuleIfDeclaredUnderAge<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "configRegionRuleIfDeclaredUnderAge" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Object used to store Age Assurance data in stash. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Event<'a> { + /// The access level granted based on Age Assurance data we've processed. + #[serde(borrow)] + pub access: jacquard_common::CowStr<'a>, + /// The unique identifier for this instance of the Age Assurance flow, in UUID format. + #[serde(borrow)] + pub attempt_id: jacquard_common::CowStr<'a>, + /// The IP address used when completing the Age Assurance flow. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub complete_ip: std::option::Option>, + /// The user agent used when completing the Age Assurance flow. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub complete_ua: std::option::Option>, + /// The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. + #[serde(borrow)] + pub country_code: jacquard_common::CowStr<'a>, + /// The date and time of this write operation. + pub created_at: jacquard_common::types::string::Datetime, + /// The email used for Age Assurance. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub email: std::option::Option>, + /// The IP address used when initiating the Age Assurance flow. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub init_ip: std::option::Option>, + /// The user agent used when initiating the Age Assurance flow. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub init_ua: std::option::Option>, + /// The ISO 3166-2 region code provided when beginning the Age Assurance flow. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub region_code: std::option::Option>, + /// The status of the Age Assurance process. + #[serde(borrow)] + pub status: jacquard_common::CowStr<'a>, +} + +pub mod event_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type CreatedAt; + type Status; + type Access; + type AttemptId; + type CountryCode; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type CreatedAt = Unset; + type Status = Unset; + type Access = Unset; + type AttemptId = Unset; + type CountryCode = Unset; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type CreatedAt = Set; + type Status = S::Status; + type Access = S::Access; + type AttemptId = S::AttemptId; + type CountryCode = S::CountryCode; + } + ///State transition - sets the `status` field to Set + pub struct SetStatus(PhantomData S>); + impl sealed::Sealed for SetStatus {} + impl State for SetStatus { + type CreatedAt = S::CreatedAt; + type Status = Set; + type Access = S::Access; + type AttemptId = S::AttemptId; + type CountryCode = S::CountryCode; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type CreatedAt = S::CreatedAt; + type Status = S::Status; + type Access = Set; + type AttemptId = S::AttemptId; + type CountryCode = S::CountryCode; + } + ///State transition - sets the `attempt_id` field to Set + pub struct SetAttemptId(PhantomData S>); + impl sealed::Sealed for SetAttemptId {} + impl State for SetAttemptId { + type CreatedAt = S::CreatedAt; + type Status = S::Status; + type Access = S::Access; + type AttemptId = Set; + type CountryCode = S::CountryCode; + } + ///State transition - sets the `country_code` field to Set + pub struct SetCountryCode(PhantomData S>); + impl sealed::Sealed for SetCountryCode {} + impl State for SetCountryCode { + type CreatedAt = S::CreatedAt; + type Status = S::Status; + type Access = S::Access; + type AttemptId = S::AttemptId; + type CountryCode = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `created_at` field + pub struct created_at(()); + ///Marker type for the `status` field + pub struct status(()); + ///Marker type for the `access` field + pub struct access(()); + ///Marker type for the `attempt_id` field + pub struct attempt_id(()); + ///Marker type for the `country_code` field + pub struct country_code(()); + } +} + +/// Builder for constructing an instance of this type +pub struct EventBuilder<'a, S: event_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option, + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> Event<'a> { + /// Create a new builder for this type + pub fn new() -> EventBuilder<'a, event_state::Empty> { + EventBuilder::new() + } +} + +impl<'a> EventBuilder<'a, event_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + EventBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> EventBuilder<'a, S> +where + S: event_state::State, + S::Access: event_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> EventBuilder<'a, event_state::SetAccess> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + EventBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> EventBuilder<'a, S> +where + S: event_state::State, + S::AttemptId: event_state::IsUnset, +{ + /// Set the `attemptId` field (required) + pub fn attempt_id( + mut self, + value: impl Into>, + ) -> EventBuilder<'a, event_state::SetAttemptId> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + EventBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: event_state::State> EventBuilder<'a, S> { + /// Set the `completeIp` field (optional) + pub fn complete_ip( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.2 = value.into(); + self + } + /// Set the `completeIp` field to an Option value (optional) + pub fn maybe_complete_ip( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.2 = value; + self + } +} + +impl<'a, S: event_state::State> EventBuilder<'a, S> { + /// Set the `completeUa` field (optional) + pub fn complete_ua( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.3 = value.into(); + self + } + /// Set the `completeUa` field to an Option value (optional) + pub fn maybe_complete_ua( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.3 = value; + self + } +} + +impl<'a, S> EventBuilder<'a, S> +where + S: event_state::State, + S::CountryCode: event_state::IsUnset, +{ + /// Set the `countryCode` field (required) + pub fn country_code( + mut self, + value: impl Into>, + ) -> EventBuilder<'a, event_state::SetCountryCode> { + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); + EventBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> EventBuilder<'a, S> +where + S: event_state::State, + S::CreatedAt: event_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> EventBuilder<'a, event_state::SetCreatedAt> { + self.__unsafe_private_named.5 = ::core::option::Option::Some(value.into()); + EventBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: event_state::State> EventBuilder<'a, S> { + /// Set the `email` field (optional) + pub fn email( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.6 = value.into(); + self + } + /// Set the `email` field to an Option value (optional) + pub fn maybe_email(mut self, value: Option>) -> Self { + self.__unsafe_private_named.6 = value; + self + } +} + +impl<'a, S: event_state::State> EventBuilder<'a, S> { + /// Set the `initIp` field (optional) + pub fn init_ip( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.7 = value.into(); + self + } + /// Set the `initIp` field to an Option value (optional) + pub fn maybe_init_ip(mut self, value: Option>) -> Self { + self.__unsafe_private_named.7 = value; + self + } +} + +impl<'a, S: event_state::State> EventBuilder<'a, S> { + /// Set the `initUa` field (optional) + pub fn init_ua( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.8 = value.into(); + self + } + /// Set the `initUa` field to an Option value (optional) + pub fn maybe_init_ua(mut self, value: Option>) -> Self { + self.__unsafe_private_named.8 = value; + self + } +} + +impl<'a, S: event_state::State> EventBuilder<'a, S> { + /// Set the `regionCode` field (optional) + pub fn region_code( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.9 = value.into(); + self + } + /// Set the `regionCode` field to an Option value (optional) + pub fn maybe_region_code( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.9 = value; + self + } +} + +impl<'a, S> EventBuilder<'a, S> +where + S: event_state::State, + S::Status: event_state::IsUnset, +{ + /// Set the `status` field (required) + pub fn status( + mut self, + value: impl Into>, + ) -> EventBuilder<'a, event_state::SetStatus> { + self.__unsafe_private_named.10 = ::core::option::Option::Some(value.into()); + EventBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> EventBuilder<'a, S> +where + S: event_state::State, + S::CreatedAt: event_state::IsSet, + S::Status: event_state::IsSet, + S::Access: event_state::IsSet, + S::AttemptId: event_state::IsSet, + S::CountryCode: event_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Event<'a> { + Event { + access: self.__unsafe_private_named.0.unwrap(), + attempt_id: self.__unsafe_private_named.1.unwrap(), + complete_ip: self.__unsafe_private_named.2, + complete_ua: self.__unsafe_private_named.3, + country_code: self.__unsafe_private_named.4.unwrap(), + created_at: self.__unsafe_private_named.5.unwrap(), + email: self.__unsafe_private_named.6, + init_ip: self.__unsafe_private_named.7, + init_ua: self.__unsafe_private_named.8, + region_code: self.__unsafe_private_named.9, + status: self.__unsafe_private_named.10.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> Event<'a> { + Event { + access: self.__unsafe_private_named.0.unwrap(), + attempt_id: self.__unsafe_private_named.1.unwrap(), + complete_ip: self.__unsafe_private_named.2, + complete_ua: self.__unsafe_private_named.3, + country_code: self.__unsafe_private_named.4.unwrap(), + created_at: self.__unsafe_private_named.5.unwrap(), + email: self.__unsafe_private_named.6, + init_ip: self.__unsafe_private_named.7, + init_ua: self.__unsafe_private_named.8, + region_code: self.__unsafe_private_named.9, + status: self.__unsafe_private_named.10.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Event<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "event" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// The user's computed Age Assurance state. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct State<'a> { + #[serde(borrow)] + pub access: crate::app_bsky::ageassurance::Access<'a>, + /// The timestamp when this state was last updated. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub last_initiated_at: std::option::Option, + #[serde(borrow)] + pub status: crate::app_bsky::ageassurance::Status<'a>, +} + +pub mod state_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Status; + type Access; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Status = Unset; + type Access = Unset; + } + ///State transition - sets the `status` field to Set + pub struct SetStatus(PhantomData S>); + impl sealed::Sealed for SetStatus {} + impl State for SetStatus { + type Status = Set; + type Access = S::Access; + } + ///State transition - sets the `access` field to Set + pub struct SetAccess(PhantomData S>); + impl sealed::Sealed for SetAccess {} + impl State for SetAccess { + type Status = S::Status; + type Access = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `status` field + pub struct status(()); + ///Marker type for the `access` field + pub struct access(()); + } +} + +/// Builder for constructing an instance of this type +pub struct StateBuilder<'a, S: state_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> State<'a> { + /// Create a new builder for this type + pub fn new() -> StateBuilder<'a, state_state::Empty> { + StateBuilder::new() + } +} + +impl<'a> StateBuilder<'a, state_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + StateBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> StateBuilder<'a, S> +where + S: state_state::State, + S::Access: state_state::IsUnset, +{ + /// Set the `access` field (required) + pub fn access( + mut self, + value: impl Into>, + ) -> StateBuilder<'a, state_state::SetAccess> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + StateBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: state_state::State> StateBuilder<'a, S> { + /// Set the `lastInitiatedAt` field (optional) + pub fn last_initiated_at( + mut self, + value: impl Into>, + ) -> Self { + self.__unsafe_private_named.1 = value.into(); + self + } + /// Set the `lastInitiatedAt` field to an Option value (optional) + pub fn maybe_last_initiated_at( + mut self, + value: Option, + ) -> Self { + self.__unsafe_private_named.1 = value; + self + } +} + +impl<'a, S> StateBuilder<'a, S> +where + S: state_state::State, + S::Status: state_state::IsUnset, +{ + /// Set the `status` field (required) + pub fn status( + mut self, + value: impl Into>, + ) -> StateBuilder<'a, state_state::SetStatus> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + StateBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> StateBuilder<'a, S> +where + S: state_state::State, + S::Status: state_state::IsSet, + S::Access: state_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> State<'a> { + State { + access: self.__unsafe_private_named.0.unwrap(), + last_initiated_at: self.__unsafe_private_named.1, + status: self.__unsafe_private_named.2.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> State<'a> { + State { + access: self.__unsafe_private_named.0.unwrap(), + last_initiated_at: self.__unsafe_private_named.1, + status: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for State<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "state" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Additional metadata needed to compute Age Assurance state client-side. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic, + Default +)] +#[serde(rename_all = "camelCase")] +pub struct StateMetadata<'a> { + /// The account creation timestamp. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub account_created_at: std::option::Option< + jacquard_common::types::string::Datetime, + >, +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for StateMetadata<'a> { + fn nsid() -> &'static str { + "app.bsky.ageassurance.defs" + } + fn def_name() -> &'static str { + "stateMetadata" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_app_bsky_ageassurance_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// The status of the Age Assurance process. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Status<'a> { + Unknown, + Pending, + Assured, + Blocked, + Other(jacquard_common::CowStr<'a>), +} + +impl<'a> Status<'a> { + pub fn as_str(&self) -> &str { + match self { + Self::Unknown => "unknown", + Self::Pending => "pending", + Self::Assured => "assured", + Self::Blocked => "blocked", + Self::Other(s) => s.as_ref(), + } + } +} + +impl<'a> From<&'a str> for Status<'a> { + fn from(s: &'a str) -> Self { + match s { + "unknown" => Self::Unknown, + "pending" => Self::Pending, + "assured" => Self::Assured, + "blocked" => Self::Blocked, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> From for Status<'a> { + fn from(s: String) -> Self { + match s.as_str() { + "unknown" => Self::Unknown, + "pending" => Self::Pending, + "assured" => Self::Assured, + "blocked" => Self::Blocked, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> AsRef for Status<'a> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl<'a> serde::Serialize for Status<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de, 'a> serde::Deserialize<'de> for Status<'a> +where + 'de: 'a, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = <&'de str>::deserialize(deserializer)?; + Ok(Self::from(s)) + } +} + +impl jacquard_common::IntoStatic for Status<'_> { + type Output = Status<'static>; + fn into_static(self) -> Self::Output { + match self { + Status::Unknown => Status::Unknown, + Status::Pending => Status::Pending, + Status::Assured => Status::Assured, + Status::Blocked => Status::Blocked, + Status::Other(v) => Status::Other(v.into_static()), + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/app_bsky/ageassurance/begin.rs b/crates/weaver-api/src/app_bsky/ageassurance/begin.rs new file mode 100644 index 0000000..7ecc36c --- /dev/null +++ b/crates/weaver-api/src/app_bsky/ageassurance/begin.rs @@ -0,0 +1,142 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: app.bsky.ageassurance.begin +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic, + Default +)] +#[serde(rename_all = "camelCase")] +pub struct Begin<'a> { + /// An ISO 3166-1 alpha-2 code of the user's location. + #[serde(borrow)] + pub country_code: jacquard_common::CowStr<'a>, + /// The user's email address to receive Age Assurance instructions. + #[serde(borrow)] + pub email: jacquard_common::CowStr<'a>, + /// The user's preferred language for communication during the Age Assurance process. + #[serde(borrow)] + pub language: jacquard_common::CowStr<'a>, + /// An optional ISO 3166-2 code of the user's region or state within the country. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub region_code: std::option::Option>, +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct BeginOutput<'a> { + #[serde(flatten)] + #[serde(borrow)] + pub value: crate::app_bsky::ageassurance::State<'a>, +} + +#[jacquard_derive::open_union] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + thiserror::Error, + miette::Diagnostic, + jacquard_derive::IntoStatic +)] +#[serde(tag = "error", content = "message")] +#[serde(bound(deserialize = "'de: 'a"))] +pub enum BeginError<'a> { + #[serde(rename = "InvalidEmail")] + InvalidEmail(std::option::Option), + #[serde(rename = "DidTooLong")] + DidTooLong(std::option::Option), + #[serde(rename = "InvalidInitiation")] + InvalidInitiation(std::option::Option), + #[serde(rename = "RegionNotSupported")] + RegionNotSupported(std::option::Option), +} + +impl std::fmt::Display for BeginError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidEmail(msg) => { + write!(f, "InvalidEmail")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::DidTooLong(msg) => { + write!(f, "DidTooLong")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::InvalidInitiation(msg) => { + write!(f, "InvalidInitiation")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::RegionNotSupported(msg) => { + write!(f, "RegionNotSupported")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), + } + } +} + +/// Response type for +///app.bsky.ageassurance.begin +pub struct BeginResponse; +impl jacquard_common::xrpc::XrpcResp for BeginResponse { + const NSID: &'static str = "app.bsky.ageassurance.begin"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = BeginOutput<'de>; + type Err<'de> = BeginError<'de>; +} + +impl<'a> jacquard_common::xrpc::XrpcRequest for Begin<'a> { + const NSID: &'static str = "app.bsky.ageassurance.begin"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = BeginResponse; +} + +/// Endpoint type for +///app.bsky.ageassurance.begin +pub struct BeginRequest; +impl jacquard_common::xrpc::XrpcEndpoint for BeginRequest { + const PATH: &'static str = "/xrpc/app.bsky.ageassurance.begin"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request<'de> = Begin<'de>; + type Response = BeginResponse; +} \ No newline at end of file diff --git a/crates/weaver-api/src/app_bsky/ageassurance/get_config.rs b/crates/weaver-api/src/app_bsky/ageassurance/get_config.rs new file mode 100644 index 0000000..4fa49a7 --- /dev/null +++ b/crates/weaver-api/src/app_bsky/ageassurance/get_config.rs @@ -0,0 +1,61 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: app.bsky.ageassurance.getConfig +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct GetConfigOutput<'a> { + #[serde(flatten)] + #[serde(borrow)] + pub value: crate::app_bsky::ageassurance::Config<'a>, +} + +/// XRPC request marker type +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + jacquard_derive::IntoStatic +)] +pub struct GetConfig; +/// Response type for +///app.bsky.ageassurance.getConfig +pub struct GetConfigResponse; +impl jacquard_common::xrpc::XrpcResp for GetConfigResponse { + const NSID: &'static str = "app.bsky.ageassurance.getConfig"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = GetConfigOutput<'de>; + type Err<'de> = jacquard_common::xrpc::GenericError<'de>; +} + +impl jacquard_common::xrpc::XrpcRequest for GetConfig { + const NSID: &'static str = "app.bsky.ageassurance.getConfig"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = GetConfigResponse; +} + +/// Endpoint type for +///app.bsky.ageassurance.getConfig +pub struct GetConfigRequest; +impl jacquard_common::xrpc::XrpcEndpoint for GetConfigRequest { + const PATH: &'static str = "/xrpc/app.bsky.ageassurance.getConfig"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = GetConfig; + type Response = GetConfigResponse; +} \ No newline at end of file diff --git a/crates/weaver-api/src/app_bsky/ageassurance/get_state.rs b/crates/weaver-api/src/app_bsky/ageassurance/get_state.rs new file mode 100644 index 0000000..a6490e6 --- /dev/null +++ b/crates/weaver-api/src/app_bsky/ageassurance/get_state.rs @@ -0,0 +1,180 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: app.bsky.ageassurance.getState +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct GetState<'a> { + #[serde(borrow)] + pub country_code: jacquard_common::CowStr<'a>, + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub region_code: std::option::Option>, +} + +pub mod get_state_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type CountryCode; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type CountryCode = Unset; + } + ///State transition - sets the `country_code` field to Set + pub struct SetCountryCode(PhantomData S>); + impl sealed::Sealed for SetCountryCode {} + impl State for SetCountryCode { + type CountryCode = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `country_code` field + pub struct country_code(()); + } +} + +/// Builder for constructing an instance of this type +pub struct GetStateBuilder<'a, S: get_state_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> GetState<'a> { + /// Create a new builder for this type + pub fn new() -> GetStateBuilder<'a, get_state_state::Empty> { + GetStateBuilder::new() + } +} + +impl<'a> GetStateBuilder<'a, get_state_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + GetStateBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetStateBuilder<'a, S> +where + S: get_state_state::State, + S::CountryCode: get_state_state::IsUnset, +{ + /// Set the `countryCode` field (required) + pub fn country_code( + mut self, + value: impl Into>, + ) -> GetStateBuilder<'a, get_state_state::SetCountryCode> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + GetStateBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: get_state_state::State> GetStateBuilder<'a, S> { + /// Set the `regionCode` field (optional) + pub fn region_code( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.1 = value.into(); + self + } + /// Set the `regionCode` field to an Option value (optional) + pub fn maybe_region_code( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.1 = value; + self + } +} + +impl<'a, S> GetStateBuilder<'a, S> +where + S: get_state_state::State, + S::CountryCode: get_state_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> GetState<'a> { + GetState { + country_code: self.__unsafe_private_named.0.unwrap(), + region_code: self.__unsafe_private_named.1, + } + } +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct GetStateOutput<'a> { + #[serde(borrow)] + pub metadata: crate::app_bsky::ageassurance::StateMetadata<'a>, + #[serde(borrow)] + pub state: crate::app_bsky::ageassurance::State<'a>, +} + +/// Response type for +///app.bsky.ageassurance.getState +pub struct GetStateResponse; +impl jacquard_common::xrpc::XrpcResp for GetStateResponse { + const NSID: &'static str = "app.bsky.ageassurance.getState"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = GetStateOutput<'de>; + type Err<'de> = jacquard_common::xrpc::GenericError<'de>; +} + +impl<'a> jacquard_common::xrpc::XrpcRequest for GetState<'a> { + const NSID: &'static str = "app.bsky.ageassurance.getState"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = GetStateResponse; +} + +/// Endpoint type for +///app.bsky.ageassurance.getState +pub struct GetStateRequest; +impl jacquard_common::xrpc::XrpcEndpoint for GetStateRequest { + const PATH: &'static str = "/xrpc/app.bsky.ageassurance.getState"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = GetState<'de>; + type Response = GetStateResponse; +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/edit.rs b/crates/weaver-api/src/sh_weaver/edit.rs index 4fb4b3e..518cf9a 100644 --- a/crates/weaver-api/src/sh_weaver/edit.rs +++ b/crates/weaver-api/src/sh_weaver/edit.rs @@ -145,6 +145,8 @@ pub enum DocRefValue<'a> { NotebookRef(Box>), #[serde(rename = "sh.weaver.edit.defs#entryRef")] EntryRef(Box>), + #[serde(rename = "sh.weaver.edit.defs#draftRef")] + DraftRef(Box>), } fn lexicon_doc_sh_weaver_edit_defs() -> ::jacquard_lexicon::lexicon::LexiconDoc< @@ -174,7 +176,8 @@ fn lexicon_doc_sh_weaver_edit_defs() -> ::jacquard_lexicon::lexicon::LexiconDoc< description: None, refs: vec![ ::jacquard_common::CowStr::new_static("#notebookRef"), - ::jacquard_common::CowStr::new_static("#entryRef") + ::jacquard_common::CowStr::new_static("#entryRef"), + ::jacquard_common::CowStr::new_static("#draftRef") ], closed: None, }), @@ -184,12 +187,12 @@ fn lexicon_doc_sh_weaver_edit_defs() -> ::jacquard_lexicon::lexicon::LexiconDoc< }), ); map.insert( - ::jacquard_common::smol_str::SmolStr::new_static("entryRef"), + ::jacquard_common::smol_str::SmolStr::new_static("draftRef"), ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { description: None, required: Some( vec![ - ::jacquard_common::smol_str::SmolStr::new_static("notebook") + ::jacquard_common::smol_str::SmolStr::new_static("draft_key") ], ), nullable: None, @@ -197,7 +200,39 @@ fn lexicon_doc_sh_weaver_edit_defs() -> ::jacquard_lexicon::lexicon::LexiconDoc< #[allow(unused_mut)] let mut map = ::std::collections::BTreeMap::new(); map.insert( - ::jacquard_common::smol_str::SmolStr::new_static("notebook"), + ::jacquard_common::smol_str::SmolStr::new_static( + "draft_key", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + format: None, + default: None, + min_length: None, + max_length: Some(200usize), + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("entryRef"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![::jacquard_common::smol_str::SmolStr::new_static("entry")], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("entry"), ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { description: None, r#ref: ::jacquard_common::CowStr::new_static( @@ -257,6 +292,53 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for DocRef<'a> { } } +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic, + Default +)] +#[serde(rename_all = "camelCase")] +pub struct DraftRef<'a> { + #[serde(borrow)] + pub draft_key: jacquard_common::CowStr<'a>, +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for DraftRef<'a> { + fn nsid() -> &'static str { + "sh.weaver.edit.defs" + } + fn def_name() -> &'static str { + "draftRef" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_edit_defs() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + { + let value = &self.draft_key; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 200usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "draft_key", + ), + max: 200usize, + actual: ::len(value.as_ref()), + }); + } + } + Ok(()) + } +} + #[jacquard_derive::lexicon] #[derive( serde::Serialize, @@ -270,7 +352,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for DocRef<'a> { #[serde(rename_all = "camelCase")] pub struct EntryRef<'a> { #[serde(borrow)] - pub notebook: crate::com_atproto::repo::strong_ref::StrongRef<'a>, + pub entry: crate::com_atproto::repo::strong_ref::StrongRef<'a>, } pub mod entry_ref_state { @@ -283,25 +365,25 @@ pub mod entry_ref_state { } /// State trait tracking which required fields have been set pub trait State: sealed::Sealed { - type Notebook; + type Entry; } /// Empty state - all required fields are unset pub struct Empty(()); impl sealed::Sealed for Empty {} impl State for Empty { - type Notebook = Unset; + type Entry = Unset; } - ///State transition - sets the `notebook` field to Set - pub struct SetNotebook(PhantomData S>); - impl sealed::Sealed for SetNotebook {} - impl State for SetNotebook { - type Notebook = Set; + ///State transition - sets the `entry` field to Set + pub struct SetEntry(PhantomData S>); + impl sealed::Sealed for SetEntry {} + impl State for SetEntry { + type Entry = Set; } /// Marker types for field names #[allow(non_camel_case_types)] pub mod members { - ///Marker type for the `notebook` field - pub struct notebook(()); + ///Marker type for the `entry` field + pub struct entry(()); } } @@ -335,13 +417,13 @@ impl<'a> EntryRefBuilder<'a, entry_ref_state::Empty> { impl<'a, S> EntryRefBuilder<'a, S> where S: entry_ref_state::State, - S::Notebook: entry_ref_state::IsUnset, + S::Entry: entry_ref_state::IsUnset, { - /// Set the `notebook` field (required) - pub fn notebook( + /// Set the `entry` field (required) + pub fn entry( mut self, value: impl Into>, - ) -> EntryRefBuilder<'a, entry_ref_state::SetNotebook> { + ) -> EntryRefBuilder<'a, entry_ref_state::SetEntry> { self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); EntryRefBuilder { _phantom_state: ::core::marker::PhantomData, @@ -354,12 +436,12 @@ where impl<'a, S> EntryRefBuilder<'a, S> where S: entry_ref_state::State, - S::Notebook: entry_ref_state::IsSet, + S::Entry: entry_ref_state::IsSet, { /// Build the final struct pub fn build(self) -> EntryRef<'a> { EntryRef { - notebook: self.__unsafe_private_named.0.unwrap(), + entry: self.__unsafe_private_named.0.unwrap(), extra_data: Default::default(), } } @@ -372,7 +454,7 @@ where >, ) -> EntryRef<'a> { EntryRef { - notebook: self.__unsafe_private_named.0.unwrap(), + entry: self.__unsafe_private_named.0.unwrap(), extra_data: Some(extra_data), } } diff --git a/crates/weaver-api/src/sh_weaver/edit/diff.rs b/crates/weaver-api/src/sh_weaver/edit/diff.rs index a573cf3..a7c7e1c 100644 --- a/crates/weaver-api/src/sh_weaver/edit/diff.rs +++ b/crates/weaver-api/src/sh_weaver/edit/diff.rs @@ -20,6 +20,9 @@ pub struct Diff<'a> { #[serde(borrow)] pub doc: crate::sh_weaver::edit::DocRef<'a>, + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub prev: std::option::Option>, #[serde(borrow)] pub root: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(borrow)] @@ -90,6 +93,7 @@ pub struct DiffBuilder<'a, S: diff_state::State> { __unsafe_private_named: ( ::core::option::Option>, ::core::option::Option>, + ::core::option::Option>, ::core::option::Option>, ), _phantom: ::core::marker::PhantomData<&'a ()>, @@ -107,7 +111,7 @@ impl<'a> DiffBuilder<'a, diff_state::Empty> { pub fn new() -> Self { DiffBuilder { _phantom_state: ::core::marker::PhantomData, - __unsafe_private_named: (None, None, None), + __unsafe_private_named: (None, None, None, None), _phantom: ::core::marker::PhantomData, } } @@ -132,6 +136,25 @@ where } } +impl<'a, S: diff_state::State> DiffBuilder<'a, S> { + /// Set the `prev` field (optional) + pub fn prev( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.1 = value.into(); + self + } + /// Set the `prev` field to an Option value (optional) + pub fn maybe_prev( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.1 = value; + self + } +} + impl<'a, S> DiffBuilder<'a, S> where S: diff_state::State, @@ -142,7 +165,7 @@ where mut self, value: impl Into>, ) -> DiffBuilder<'a, diff_state::SetRoot> { - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); DiffBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -161,7 +184,7 @@ where mut self, value: impl Into>, ) -> DiffBuilder<'a, diff_state::SetSnapshot> { - self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); DiffBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -181,8 +204,9 @@ where pub fn build(self) -> Diff<'a> { Diff { doc: self.__unsafe_private_named.0.unwrap(), - root: self.__unsafe_private_named.1.unwrap(), - snapshot: self.__unsafe_private_named.2.unwrap(), + prev: self.__unsafe_private_named.1, + root: self.__unsafe_private_named.2.unwrap(), + snapshot: self.__unsafe_private_named.3.unwrap(), extra_data: Default::default(), } } @@ -196,8 +220,9 @@ where ) -> Diff<'a> { Diff { doc: self.__unsafe_private_named.0.unwrap(), - root: self.__unsafe_private_named.1.unwrap(), - snapshot: self.__unsafe_private_named.2.unwrap(), + prev: self.__unsafe_private_named.1, + root: self.__unsafe_private_named.2.unwrap(), + snapshot: self.__unsafe_private_named.3.unwrap(), extra_data: Some(extra_data), } } @@ -322,6 +347,15 @@ fn lexicon_doc_sh_weaver_edit_diff() -> ::jacquard_lexicon::lexicon::LexiconDoc< ), }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("prev"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", + ), + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static("root"), ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { diff --git a/crates/weaver-api/src/tools_ozone/moderation.rs b/crates/weaver-api/src/tools_ozone/moderation.rs index 4edbf61..ebb4afd 100644 --- a/crates/weaver-api/src/tools_ozone/moderation.rs +++ b/crates/weaver-api/src/tools_ozone/moderation.rs @@ -627,6 +627,15 @@ fn lexicon_doc_tools_ozone_moderation_defs() -> ::jacquard_lexicon::lexicon::Lex properties: { #[allow(unused_mut)] let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static( "attemptId", @@ -690,6 +699,27 @@ fn lexicon_doc_tools_ozone_moderation_defs() -> ::jacquard_lexicon::lexicon::Lex known_values: None, }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "countryCode", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static( "createdAt", @@ -751,12 +781,33 @@ fn lexicon_doc_tools_ozone_moderation_defs() -> ::jacquard_lexicon::lexicon::Lex known_values: None, }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "regionCode", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The ISO 3166-2 region code provided when beginning the Age Assurance flow.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static("status"), ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { description: Some( ::jacquard_common::CowStr::new_static( - "The status of the age assurance process.", + "The status of the Age Assurance process.", ), ), format: None, @@ -794,6 +845,15 @@ fn lexicon_doc_tools_ozone_moderation_defs() -> ::jacquard_lexicon::lexicon::Lex properties: { #[allow(unused_mut)] let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("access"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "app.bsky.ageassurance.defs#access", + ), + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static("comment"), ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { @@ -4990,6 +5050,9 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for AccountStrike<'a> { )] #[serde(rename_all = "camelCase")] pub struct AgeAssuranceEvent<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub access: std::option::Option>, /// The unique identifier for this instance of the age assurance flow, in UUID format. #[serde(borrow)] pub attempt_id: jacquard_common::CowStr<'a>, @@ -5001,6 +5064,10 @@ pub struct AgeAssuranceEvent<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] pub complete_ua: std::option::Option>, + /// The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub country_code: std::option::Option>, /// The date and time of this write operation. pub created_at: jacquard_common::types::string::Datetime, /// The IP address used when initiating the AA flow. @@ -5011,7 +5078,11 @@ pub struct AgeAssuranceEvent<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] pub init_ua: std::option::Option>, - /// The status of the age assurance process. + /// The ISO 3166-2 region code provided when beginning the Age Assurance flow. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub region_code: std::option::Option>, + /// The status of the Age Assurance process. #[serde(borrow)] pub status: jacquard_common::CowStr<'a>, } @@ -5078,6 +5149,8 @@ pub mod age_assurance_event_state { pub struct AgeAssuranceEventBuilder<'a, S: age_assurance_event_state::State> { _phantom_state: ::core::marker::PhantomData S>, __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option>, ::core::option::Option>, ::core::option::Option>, ::core::option::Option>, @@ -5085,6 +5158,7 @@ pub struct AgeAssuranceEventBuilder<'a, S: age_assurance_event_state::State> { ::core::option::Option>, ::core::option::Option>, ::core::option::Option>, + ::core::option::Option>, ), _phantom: ::core::marker::PhantomData<&'a ()>, } @@ -5101,12 +5175,42 @@ impl<'a> AgeAssuranceEventBuilder<'a, age_assurance_event_state::Empty> { pub fn new() -> Self { AgeAssuranceEventBuilder { _phantom_state: ::core::marker::PhantomData, - __unsafe_private_named: (None, None, None, None, None, None, None), + __unsafe_private_named: ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ), _phantom: ::core::marker::PhantomData, } } } +impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { + /// Set the `access` field (optional) + pub fn access( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.0 = value.into(); + self + } + /// Set the `access` field to an Option value (optional) + pub fn maybe_access( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.0 = value; + self + } +} + impl<'a, S> AgeAssuranceEventBuilder<'a, S> where S: age_assurance_event_state::State, @@ -5117,7 +5221,7 @@ where mut self, value: impl Into>, ) -> AgeAssuranceEventBuilder<'a, age_assurance_event_state::SetAttemptId> { - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); AgeAssuranceEventBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -5132,7 +5236,7 @@ impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { mut self, value: impl Into>>, ) -> Self { - self.__unsafe_private_named.1 = value.into(); + self.__unsafe_private_named.2 = value.into(); self } /// Set the `completeIp` field to an Option value (optional) @@ -5140,7 +5244,7 @@ impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { mut self, value: Option>, ) -> Self { - self.__unsafe_private_named.1 = value; + self.__unsafe_private_named.2 = value; self } } @@ -5151,7 +5255,7 @@ impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { mut self, value: impl Into>>, ) -> Self { - self.__unsafe_private_named.2 = value.into(); + self.__unsafe_private_named.3 = value.into(); self } /// Set the `completeUa` field to an Option value (optional) @@ -5159,7 +5263,26 @@ impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { mut self, value: Option>, ) -> Self { - self.__unsafe_private_named.2 = value; + self.__unsafe_private_named.3 = value; + self + } +} + +impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { + /// Set the `countryCode` field (optional) + pub fn country_code( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.4 = value.into(); + self + } + /// Set the `countryCode` field to an Option value (optional) + pub fn maybe_country_code( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.4 = value; self } } @@ -5174,7 +5297,7 @@ where mut self, value: impl Into, ) -> AgeAssuranceEventBuilder<'a, age_assurance_event_state::SetCreatedAt> { - self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.5 = ::core::option::Option::Some(value.into()); AgeAssuranceEventBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -5189,12 +5312,12 @@ impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { mut self, value: impl Into>>, ) -> Self { - self.__unsafe_private_named.4 = value.into(); + self.__unsafe_private_named.6 = value.into(); self } /// Set the `initIp` field to an Option value (optional) pub fn maybe_init_ip(mut self, value: Option>) -> Self { - self.__unsafe_private_named.4 = value; + self.__unsafe_private_named.6 = value; self } } @@ -5205,12 +5328,31 @@ impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { mut self, value: impl Into>>, ) -> Self { - self.__unsafe_private_named.5 = value.into(); + self.__unsafe_private_named.7 = value.into(); self } /// Set the `initUa` field to an Option value (optional) pub fn maybe_init_ua(mut self, value: Option>) -> Self { - self.__unsafe_private_named.5 = value; + self.__unsafe_private_named.7 = value; + self + } +} + +impl<'a, S: age_assurance_event_state::State> AgeAssuranceEventBuilder<'a, S> { + /// Set the `regionCode` field (optional) + pub fn region_code( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.8 = value.into(); + self + } + /// Set the `regionCode` field to an Option value (optional) + pub fn maybe_region_code( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.8 = value; self } } @@ -5225,7 +5367,7 @@ where mut self, value: impl Into>, ) -> AgeAssuranceEventBuilder<'a, age_assurance_event_state::SetStatus> { - self.__unsafe_private_named.6 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.9 = ::core::option::Option::Some(value.into()); AgeAssuranceEventBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -5244,13 +5386,16 @@ where /// Build the final struct pub fn build(self) -> AgeAssuranceEvent<'a> { AgeAssuranceEvent { - attempt_id: self.__unsafe_private_named.0.unwrap(), - complete_ip: self.__unsafe_private_named.1, - complete_ua: self.__unsafe_private_named.2, - created_at: self.__unsafe_private_named.3.unwrap(), - init_ip: self.__unsafe_private_named.4, - init_ua: self.__unsafe_private_named.5, - status: self.__unsafe_private_named.6.unwrap(), + access: self.__unsafe_private_named.0, + attempt_id: self.__unsafe_private_named.1.unwrap(), + complete_ip: self.__unsafe_private_named.2, + complete_ua: self.__unsafe_private_named.3, + country_code: self.__unsafe_private_named.4, + created_at: self.__unsafe_private_named.5.unwrap(), + init_ip: self.__unsafe_private_named.6, + init_ua: self.__unsafe_private_named.7, + region_code: self.__unsafe_private_named.8, + status: self.__unsafe_private_named.9.unwrap(), extra_data: Default::default(), } } @@ -5263,13 +5408,16 @@ where >, ) -> AgeAssuranceEvent<'a> { AgeAssuranceEvent { - attempt_id: self.__unsafe_private_named.0.unwrap(), - complete_ip: self.__unsafe_private_named.1, - complete_ua: self.__unsafe_private_named.2, - created_at: self.__unsafe_private_named.3.unwrap(), - init_ip: self.__unsafe_private_named.4, - init_ua: self.__unsafe_private_named.5, - status: self.__unsafe_private_named.6.unwrap(), + access: self.__unsafe_private_named.0, + attempt_id: self.__unsafe_private_named.1.unwrap(), + complete_ip: self.__unsafe_private_named.2, + complete_ua: self.__unsafe_private_named.3, + country_code: self.__unsafe_private_named.4, + created_at: self.__unsafe_private_named.5.unwrap(), + init_ip: self.__unsafe_private_named.6, + init_ua: self.__unsafe_private_named.7, + region_code: self.__unsafe_private_named.8, + status: self.__unsafe_private_named.9.unwrap(), extra_data: Some(extra_data), } } @@ -5306,6 +5454,9 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for AgeAssuranceEvent<'a> { )] #[serde(rename_all = "camelCase")] pub struct AgeAssuranceOverrideEvent<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub access: std::option::Option>, /// Comment describing the reason for the override. #[serde(borrow)] pub comment: jacquard_common::CowStr<'a>, @@ -12247,20 +12398,28 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ScheduledActionView<'a> { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SubjectReviewState<'a> { - ReviewOpen, - ReviewEscalated, - ReviewClosed, - ReviewNone, + ToolsOzoneModerationDefsReviewOpen, + ToolsOzoneModerationDefsReviewEscalated, + ToolsOzoneModerationDefsReviewClosed, + ToolsOzoneModerationDefsReviewNone, Other(jacquard_common::CowStr<'a>), } impl<'a> SubjectReviewState<'a> { pub fn as_str(&self) -> &str { match self { - Self::ReviewOpen => "#reviewOpen", - Self::ReviewEscalated => "#reviewEscalated", - Self::ReviewClosed => "#reviewClosed", - Self::ReviewNone => "#reviewNone", + Self::ToolsOzoneModerationDefsReviewOpen => { + "tools.ozone.moderation.defs#reviewOpen" + } + Self::ToolsOzoneModerationDefsReviewEscalated => { + "tools.ozone.moderation.defs#reviewEscalated" + } + Self::ToolsOzoneModerationDefsReviewClosed => { + "tools.ozone.moderation.defs#reviewClosed" + } + Self::ToolsOzoneModerationDefsReviewNone => { + "tools.ozone.moderation.defs#reviewNone" + } Self::Other(s) => s.as_ref(), } } @@ -12269,10 +12428,18 @@ impl<'a> SubjectReviewState<'a> { impl<'a> From<&'a str> for SubjectReviewState<'a> { fn from(s: &'a str) -> Self { match s { - "#reviewOpen" => Self::ReviewOpen, - "#reviewEscalated" => Self::ReviewEscalated, - "#reviewClosed" => Self::ReviewClosed, - "#reviewNone" => Self::ReviewNone, + "tools.ozone.moderation.defs#reviewOpen" => { + Self::ToolsOzoneModerationDefsReviewOpen + } + "tools.ozone.moderation.defs#reviewEscalated" => { + Self::ToolsOzoneModerationDefsReviewEscalated + } + "tools.ozone.moderation.defs#reviewClosed" => { + Self::ToolsOzoneModerationDefsReviewClosed + } + "tools.ozone.moderation.defs#reviewNone" => { + Self::ToolsOzoneModerationDefsReviewNone + } _ => Self::Other(jacquard_common::CowStr::from(s)), } } @@ -12281,10 +12448,18 @@ impl<'a> From<&'a str> for SubjectReviewState<'a> { impl<'a> From for SubjectReviewState<'a> { fn from(s: String) -> Self { match s.as_str() { - "#reviewOpen" => Self::ReviewOpen, - "#reviewEscalated" => Self::ReviewEscalated, - "#reviewClosed" => Self::ReviewClosed, - "#reviewNone" => Self::ReviewNone, + "tools.ozone.moderation.defs#reviewOpen" => { + Self::ToolsOzoneModerationDefsReviewOpen + } + "tools.ozone.moderation.defs#reviewEscalated" => { + Self::ToolsOzoneModerationDefsReviewEscalated + } + "tools.ozone.moderation.defs#reviewClosed" => { + Self::ToolsOzoneModerationDefsReviewClosed + } + "tools.ozone.moderation.defs#reviewNone" => { + Self::ToolsOzoneModerationDefsReviewNone + } _ => Self::Other(jacquard_common::CowStr::from(s)), } } @@ -12322,10 +12497,18 @@ impl jacquard_common::IntoStatic for SubjectReviewState<'_> { type Output = SubjectReviewState<'static>; fn into_static(self) -> Self::Output { match self { - SubjectReviewState::ReviewOpen => SubjectReviewState::ReviewOpen, - SubjectReviewState::ReviewEscalated => SubjectReviewState::ReviewEscalated, - SubjectReviewState::ReviewClosed => SubjectReviewState::ReviewClosed, - SubjectReviewState::ReviewNone => SubjectReviewState::ReviewNone, + SubjectReviewState::ToolsOzoneModerationDefsReviewOpen => { + SubjectReviewState::ToolsOzoneModerationDefsReviewOpen + } + SubjectReviewState::ToolsOzoneModerationDefsReviewEscalated => { + SubjectReviewState::ToolsOzoneModerationDefsReviewEscalated + } + SubjectReviewState::ToolsOzoneModerationDefsReviewClosed => { + SubjectReviewState::ToolsOzoneModerationDefsReviewClosed + } + SubjectReviewState::ToolsOzoneModerationDefsReviewNone => { + SubjectReviewState::ToolsOzoneModerationDefsReviewNone + } SubjectReviewState::Other(v) => SubjectReviewState::Other(v.into_static()), } } diff --git a/crates/weaver-app/Cargo.toml b/crates/weaver-app/Cargo.toml index 3ca98ac..c0e5bcb 100644 --- a/crates/weaver-app/Cargo.toml +++ b/crates/weaver-app/Cargo.toml @@ -47,6 +47,7 @@ syntect = { workspace = true, default-features = false, features = ["default-fan # diesel_migrations = { version = "2.3", features = ["sqlite"] } tokio = { version = "1.28", features = ["sync"] } serde_html_form = "0.2.8" +regex-lite = "0.1" tracing.workspace = true serde_ipld_dagcbor = { version = "0.6" } loro = "1.9.1" diff --git a/crates/weaver-app/public/sw.js b/crates/weaver-app/public/sw.js index 150f37a..879cfc7 100644 --- a/crates/weaver-app/public/sw.js +++ b/crates/weaver-app/public/sw.js @@ -1,9 +1,15 @@ // Weaver Service Worker -// Handles blob/image requests by intercepting fetch and serving from PDS +// Handles blob/image requests by caching immutable images +// +// URL patterns handled: +// - /image/{ident}/draft/{blob_rkey}/{name} - draft images (unpublished) +// - /image/{ident}/{entry_rkey}/{name} - published entry images +// - /image/{notebook}/{name} - notebook images (legacy) +// - /{notebook}/image/{name} - notebook images (legacy path) -const CACHE_NAME = "weaver-blobs-v1"; +const CACHE_NAME = "weaver-blobs-v2"; -// Map of notebook/path -> real URL +// Map of notebook/path -> real URL for legacy blob mappings // e.g., "notebook/image/foo.jpg" -> "https://pds.example.com/xrpc/com.atproto.sync.getBlob?..." const urlMappings = new Map(); @@ -13,10 +19,22 @@ self.addEventListener("install", (event) => { }); self.addEventListener("activate", (event) => { - event.waitUntil(clients.claim()); + event.waitUntil( + Promise.all([ + clients.claim(), + // Clean up old cache versions + caches.keys().then((names) => + Promise.all( + names + .filter((name) => name.startsWith("weaver-blobs-") && name !== CACHE_NAME) + .map((name) => caches.delete(name)) + ) + ), + ]) + ); }); -// Receive mappings from main thread +// Receive mappings from main thread (for legacy notebook images) self.addEventListener("message", (event) => { if (event.data.type === "register_mappings") { const notebook = event.data.notebook; @@ -28,33 +46,59 @@ self.addEventListener("message", (event) => { } }); +// Check if a path is an image request we should cache +function isImagePath(pathname) { + // New format: /image/{ident}/{...}/{name} + if (pathname.startsWith("/image/")) { + return true; + } + // Legacy format: /{notebook}/image/{name} + const parts = pathname.split("/").filter((p) => p); + if (parts.length >= 3 && parts[parts.length - 2] === "image") { + return true; + } + return false; +} + // Intercept fetch requests self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); - // Extract key from path (e.g., "/notebook/image/foo.jpg" -> "notebook/image/foo.jpg") + // Only handle same-origin requests + if (url.origin !== self.location.origin) { + return; + } + + // Check if this is an image request + if (!isImagePath(url.pathname)) { + return; + } + + // Use pathname as cache key + const cacheKey = url.pathname; + + // Extract path parts for legacy mapping lookup const pathParts = url.pathname.split("/").filter((p) => p); - // Check if this looks like an image request (format: /:notebook/image/:name) + // Check legacy mappings (for /{notebook}/image/{name} format) if (pathParts.length >= 3 && pathParts[pathParts.length - 2] === "image") { - // Reconstruct the key - const key = pathParts.join("/"); - - const mapping = urlMappings.get(key); + const legacyKey = pathParts.join("/"); + const mapping = urlMappings.get(legacyKey); if (mapping) { - event.respondWith(handleBlobRequest(mapping, key)); + event.respondWith(handleBlobRequest(mapping, cacheKey)); return; } } - // Let other requests pass through + // For new /image/... routes, cache the response from our server + event.respondWith(handleImageRequest(event.request, cacheKey)); }); -async function handleBlobRequest(url, key) { +// Handle requests that have a direct URL mapping (legacy) +async function handleBlobRequest(url, cacheKey) { try { - // Check cache first const cache = await caches.open(CACHE_NAME); - let response = await cache.match(key); + let response = await cache.match(cacheKey); if (response) { return response; @@ -68,8 +112,8 @@ async function handleBlobRequest(url, key) { return new Response("Blob not found", { status: 404 }); } - // Cache the response - await cache.put(key, response.clone()); + // Cache the response (blobs are immutable by CID) + await cache.put(cacheKey, response.clone()); return response; } catch (error) { @@ -77,3 +121,35 @@ async function handleBlobRequest(url, key) { return new Response("Error fetching blob", { status: 500 }); } } + +// Handle image requests via our server (new /image/... routes) +async function handleImageRequest(request, cacheKey) { + try { + const cache = await caches.open(CACHE_NAME); + let response = await cache.match(cacheKey); + + if (response) { + return response; + } + + // Fetch from our server + response = await fetch(request); + + if (!response.ok) { + // Don't cache error responses + return response; + } + + // Check if response is cacheable (has immutable cache-control) + const cacheControl = response.headers.get("cache-control") || ""; + if (cacheControl.includes("immutable") || cacheControl.includes("max-age=31536000")) { + // Cache the response + await cache.put(cacheKey, response.clone()); + } + + return response; + } catch (error) { + console.error("[SW] Error handling image request:", error); + return new Response("Error fetching image", { status: 500 }); + } +} diff --git a/crates/weaver-app/src/blobcache.rs b/crates/weaver-app/src/blobcache.rs index e070877..f13b20b 100644 --- a/crates/weaver-app/src/blobcache.rs +++ b/crates/weaver-app/src/blobcache.rs @@ -6,13 +6,18 @@ use jacquard::{ identity::JacquardResolver, prelude::*, smol_str::SmolStr, - types::{cid::Cid, ident::AtIdentifier}, + types::{cid::Cid, collection::Collection, ident::AtIdentifier, nsid::Nsid, string::Rkey}, + xrpc::XrpcExt, + IntoStatic, }; use std::{ - sync::{Arc, Mutex}, + sync::Arc, time::Duration, }; +use weaver_api::com_atproto::repo::get_record::GetRecord; use weaver_api::com_atproto::sync::get_blob::GetBlob; +use weaver_api::sh_weaver::notebook::entry::Entry; +use weaver_api::sh_weaver::publish::blob::Blob as PublishedBlob; #[derive(Clone)] pub struct BlobCache { @@ -29,44 +34,68 @@ impl BlobCache { Self { client, cache, map } } - pub async fn cache( + /// Resolve DID and PDS URL from an identifier + async fn resolve_ident( &self, - ident: AtIdentifier<'static>, - cid: Cid<'static>, - name: Option, - ) -> Result<()> { - let (repo_did, pds_url) = match ident { + ident: &AtIdentifier<'_>, + ) -> Result<(jacquard::types::string::Did<'static>, jacquard::url::Url)> { + match ident { AtIdentifier::Did(did) => { - let pds = self.client.pds_for_did(&did).await?; - (did.clone(), pds) + let pds = self.client.pds_for_did(did).await?; + Ok((did.clone().into_static(), pds)) + } + AtIdentifier::Handle(handle) => { + let (did, pds) = self.client.pds_for_handle(handle).await?; + Ok((did, pds)) } - AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, - }; - if self.get_cid(&cid).is_some() { - return Ok(()); } - let blob = if let Ok(blob_stream) = self + } + + /// Fetch a blob by CID from a specific DID's PDS + async fn fetch_blob( + &self, + did: &jacquard::types::string::Did<'_>, + pds_url: jacquard::url::Url, + cid: &Cid<'_>, + ) -> Result { + if let Ok(blob_stream) = self .client .xrpc(pds_url) .send( &GetBlob::new() .cid(cid.clone()) - .did(repo_did.clone()) + .did(did.clone()) .build(), ) .await { - blob_stream.buffer().clone() + Ok(blob_stream.buffer().clone()) } else { - reqwest::get(format!( + // Fallback to Bluesky CDN (works for blobs stored on bsky PDSes) + let bytes = reqwest::get(format!( "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", - repo_did, cid + did, cid )) .await? .bytes() - .await? - .clone() - }; + .await?; + Ok(bytes) + } + } + + pub async fn cache( + &self, + ident: AtIdentifier<'static>, + cid: Cid<'static>, + name: Option, + ) -> Result<()> { + let (repo_did, pds_url) = self.resolve_ident(&ident).await?; + + if self.get_cid(&cid).is_some() { + return Ok(()); + } + + let blob = self.fetch_blob(&repo_did, pds_url, &cid).await?; self.cache.insert(cid.clone(), blob); if let Some(name) = name { @@ -76,6 +105,133 @@ impl BlobCache { Ok(()) } + /// Resolve an image from a published entry by name. + /// + /// Looks up the entry record at `{ident}/sh.weaver.notebook.entry/{rkey}`, + /// finds the image by name in the embeds, and returns the blob bytes. + pub async fn resolve_from_entry( + &self, + ident: &AtIdentifier<'_>, + rkey: &str, + name: &str, + ) -> Result { + let (repo_did, pds_url) = self.resolve_ident(ident).await?; + + // Fetch the entry record + let resp = self + .client + .xrpc(pds_url.clone()) + .send( + &GetRecord::new() + .repo(AtIdentifier::Did(repo_did.clone())) + .collection(Nsid::raw(::NSID)) + .rkey(Rkey::new(rkey).map_err(|e| CapturedError::from_display(e))?) + .build(), + ) + .await + .map_err(|e| CapturedError::from_display(format!("Failed to fetch entry: {}", e)))?; + + let record = resp + .into_output() + .map_err(|e| CapturedError::from_display(format!("Failed to parse entry: {}", e)))?; + + // Parse the entry + let entry: Entry = jacquard::from_data(&record.value) + .map_err(|e| CapturedError::from_display(format!("Failed to deserialize entry: {}", e)))?; + + // Find the image by name + let cid = entry + .embeds + .as_ref() + .and_then(|e| e.images.as_ref()) + .and_then(|imgs| { + imgs.images.iter().find(|img| { + img.name.as_ref().map(|n| n.as_ref()) == Some(name) + }) + }) + .map(|img| img.image.blob().cid().clone().into_static()) + .ok_or_else(|| CapturedError::from_display(format!("Image '{}' not found in entry", name)))?; + + // Check cache first + if let Some(bytes) = self.get_cid(&cid) { + return Ok(bytes); + } + + // Fetch and cache the blob + let blob = self.fetch_blob(&repo_did, pds_url, &cid).await?; + self.cache.insert(cid.clone(), blob.clone()); + self.map.insert(name.into(), cid); + + Ok(blob) + } + + /// Resolve an image from a draft (unpublished) entry via PublishedBlob record. + /// + /// Looks up the PublishedBlob record at `{ident}/sh.weaver.publish.blob/{blob_rkey}`, + /// gets the CID from it, and returns the blob bytes. + pub async fn resolve_from_draft( + &self, + ident: &AtIdentifier<'_>, + blob_rkey: &str, + ) -> Result { + let (repo_did, pds_url) = self.resolve_ident(ident).await?; + + // Fetch the PublishedBlob record + let resp = self + .client + .xrpc(pds_url.clone()) + .send( + &GetRecord::new() + .repo(AtIdentifier::Did(repo_did.clone())) + .collection(Nsid::raw(::NSID)) + .rkey(Rkey::new(blob_rkey).map_err(|e| CapturedError::from_display(e))?) + .build(), + ) + .await + .map_err(|e| CapturedError::from_display(format!("Failed to fetch PublishedBlob: {}", e)))?; + + let record = resp + .into_output() + .map_err(|e| CapturedError::from_display(format!("Failed to parse PublishedBlob: {}", e)))?; + + // Parse the PublishedBlob + let published: PublishedBlob = jacquard::from_data(&record.value) + .map_err(|e| CapturedError::from_display(format!("Failed to deserialize PublishedBlob: {}", e)))?; + + // Get CID from the upload blob ref + let cid = published.upload.blob().cid().clone().into_static(); + + // Check cache first + if let Some(bytes) = self.get_cid(&cid) { + return Ok(bytes); + } + + // Fetch and cache the blob + let blob = self.fetch_blob(&repo_did, pds_url, &cid).await?; + self.cache.insert(cid, blob.clone()); + + Ok(blob) + } + + /// Resolve an image from a notebook entry by name. + /// + /// This is a convenience method that looks up the notebook, finds the entry, + /// and resolves the image. Used for `/image/{notebook}/{name}` paths. + pub async fn resolve_from_notebook( + &self, + notebook_title: &str, + image_name: &str, + ) -> Result { + // For now, just try to get from the name cache + // Full notebook resolution would require fetching the notebook record + // and iterating through entries to find the image + self.get_named(&image_name.into()) + .ok_or_else(|| CapturedError::from_display(format!( + "Image '{}' not found in notebook '{}'", + image_name, notebook_title + ))) + } + pub fn get_cid(&self, cid: &Cid<'static>) -> Option { self.cache.get(cid) } diff --git a/crates/weaver-app/src/components/editor/component.rs b/crates/weaver-app/src/components/editor/component.rs index 0d18e0c..13767fa 100644 --- a/crates/weaver-app/src/components/editor/component.rs +++ b/crates/weaver-app/src/components/editor/component.rs @@ -1,8 +1,10 @@ //! The main MarkdownEditor component. use dioxus::prelude::*; +use jacquard::IntoStatic; use jacquard::cowstr::ToCowStr; use jacquard::types::blob::BlobRef; +use jacquard::types::ident::AtIdentifier; use weaver_api::sh_weaver::embed::images::Image; use weaver_common::WeaverExt; @@ -11,25 +13,113 @@ use crate::components::editor::ReportButton; use crate::fetch::Fetcher; use super::document::{CompositionState, EditorDocument}; -use super::dom_sync::{sync_cursor_from_dom, sync_cursor_from_dom_with_direction, update_paragraph_dom}; -use super::offset_map::SnapDirection; +use super::dom_sync::{ + sync_cursor_from_dom, sync_cursor_from_dom_with_direction, update_paragraph_dom, +}; use super::formatting; use super::input::{ get_char_at, handle_copy, handle_cut, handle_keydown, handle_paste, should_intercept_key, }; +use super::offset_map::SnapDirection; use super::paragraph::ParagraphRender; use super::platform; -use super::publish::PublishButton; +use super::publish::{LoadedEntry, PublishButton, load_entry_for_editing}; use super::render; use super::storage; use super::toolbar::EditorToolbar; use super::visibility::update_syntax_visibility; use super::writer::{EditorImageResolver, SyntaxSpanInfo}; -/// Main markdown editor component. +/// Result of loading an entry - either loaded, failed, or not needed. +#[derive(Clone, PartialEq)] +enum LoadResult { + Loaded(LoadedEntry), + Failed, + NotNeeded, +} + +/// Wrapper component that handles loading an existing entry before rendering the editor. /// /// # Props -/// - `initial_content`: Optional initial markdown content +/// - `initial_content`: Optional initial markdown content (for new entries) +/// - `entry_uri`: Optional AT-URI of an existing entry to edit +#[component] +pub fn MarkdownEditor(initial_content: Option, entry_uri: Option) -> Element { + let fetcher = use_context::(); + + // Determine draft key - use entry URI if editing existing, otherwise "current" + let draft_key = entry_uri.clone().unwrap_or_else(|| "current".to_string()); + + // Check if we have a local draft first + let has_local_draft = use_hook(|| storage::load_from_storage(&draft_key).is_some()); + + // If we have an entry_uri but no local draft, we need to fetch from PDS + let needs_fetch = entry_uri.is_some() && !has_local_draft; + + // Resource returns the load result + let entry_resource = use_resource(move || { + let fetcher = fetcher.clone(); + let uri_str = entry_uri.clone(); + async move { + if !needs_fetch { + return LoadResult::NotNeeded; + } + if let Some(uri_str) = uri_str { + if let Ok(uri) = jacquard::types::string::AtUri::new(&uri_str) { + match load_entry_for_editing(&fetcher, &uri).await { + Ok(loaded) => return LoadResult::Loaded(loaded), + Err(e) => { + tracing::error!("Failed to load entry: {}", e); + return LoadResult::Failed; + } + } + } + } + LoadResult::Failed + } + }); + + // Render based on resource state + match &*entry_resource.read() { + Some(LoadResult::Loaded(loaded)) => { + rsx! { + MarkdownEditorInner { + key: "{draft_key}", + draft_key: draft_key.clone(), + loaded_entry: Some(loaded.clone()), + initial_content: None, + } + } + } + Some(LoadResult::Failed) => { + rsx! { + div { class: "editor-error", + "Failed to load entry. It may not exist or you may not have access." + } + } + } + Some(LoadResult::NotNeeded) => { + rsx! { + MarkdownEditorInner { + key: "{draft_key}", + draft_key: draft_key.clone(), + loaded_entry: None, + initial_content: initial_content.clone(), + } + } + } + None => { + // Still loading + rsx! { + div { class: "editor-loading", + "Loading entry..." + } + } + } + } +} + +/// Inner markdown editor component (actual editor implementation). /// /// # Features /// - Loro CRDT-based text storage with undo/redo support @@ -38,18 +128,26 @@ use super::writer::{EditorImageResolver, SyntaxSpanInfo}; /// - LocalStorage auto-save with debouncing /// - Keyboard shortcuts (Ctrl+B for bold, Ctrl+I for italic) #[component] -pub fn MarkdownEditor(initial_content: Option) -> Element { +fn MarkdownEditorInner( + draft_key: String, + loaded_entry: Option, + initial_content: Option, +) -> Element { // Context for authenticated API calls let fetcher = use_context::(); let auth_state = use_context::>(); - // Try to restore from localStorage (includes CRDT state for undo history) - // Use "current" as the default draft key for now - let draft_key = "current"; - // Document is NOT in a signal - its fields are individually reactive let mut document = use_hook(|| { - storage::load_from_storage(draft_key) - .unwrap_or_else(|| EditorDocument::new(initial_content.clone().unwrap_or_default())) + // Priority: loaded_entry > local storage > new with initial_content + if let Some(ref loaded) = loaded_entry { + let doc = EditorDocument::from_entry(&loaded.entry, loaded.entry_ref.clone()); + // Save to local storage so future visits use the draft + storage::save_to_storage(&doc, &draft_key).ok(); + doc + } else { + storage::load_from_storage(&draft_key) + .unwrap_or_else(|| EditorDocument::new(initial_content.clone().unwrap_or_default())) + } }); let editor_id = "markdown-editor"; @@ -149,9 +247,12 @@ pub fn MarkdownEditor(initial_content: Option) -> Element { // Use requestAnimationFrame to wait for browser paint if let Some(window) = web_sys::window() { let closure = Closure::once(move || { - if let Err(e) = - super::cursor::restore_cursor_position(cursor_offset, &map, editor_id, snap_direction) - { + if let Err(e) = super::cursor::restore_cursor_position( + cursor_offset, + &map, + editor_id, + snap_direction, + ) { tracing::warn!("Cursor restoration failed: {:?}", e); } }); @@ -175,9 +276,12 @@ pub fn MarkdownEditor(initial_content: Option) -> Element { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] let doc_for_autosave = document.clone(); #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + let draft_key_for_autosave = draft_key.clone(); + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] use_effect(move || { // Check every 500ms if there are unsaved changes let mut doc = doc_for_autosave.clone(); + let draft_key = draft_key_for_autosave.clone(); let interval = gloo_timers::callback::Interval::new(500, move || { let current_frontiers = doc.state_frontiers(); @@ -192,7 +296,7 @@ pub fn MarkdownEditor(initial_content: Option) -> Element { if needs_save { doc.sync_loro_cursor(); - let _ = storage::save_to_storage(&doc, draft_key); + let _ = storage::save_to_storage(&doc, &draft_key); // Update last saved frontiers last_saved_frontiers.set(Some(current_frontiers)); @@ -783,39 +887,29 @@ pub fn MarkdownEditor(initial_content: Option) -> Element { // Upload blob and create temporary PublishedBlob record match client.publish_blob(data, &name_for_upload, None).await { Ok((strong_ref, published_blob)) => { - // Extract the blob from PublishedBlob - let blob = match published_blob.upload { - BlobRef::Blob(b) => b, - _ => { - tracing::warn!("Unexpected BlobRef variant"); - return; - } - }; - - // Get format from mime type - let format = blob - .mime_type - .0 - .strip_prefix("image/") - .unwrap_or("jpeg") - .to_string(); - // Get DID from fetcher let did = match fetcher.current_did().await { - Some(d) => d.to_string(), + Some(d) => d, None => { tracing::warn!("No DID available"); return; } }; - let cid = blob.cid().to_string(); + // Extract rkey from the AT-URI + let blob_rkey = match strong_ref.uri.rkey() { + Some(rkey) => rkey.0.clone().into_static(), + None => { + tracing::warn!("No rkey in PublishedBlob URI"); + return; + } + }; // Build Image using the builder API let name_for_resolver = name_for_upload.clone(); let image = Image::new() .alt(alt_for_upload.to_cowstr()) - .image(BlobRef::Blob(blob)) + .image(published_blob.upload) .name(name_for_upload.to_cowstr()) .build(); @@ -823,12 +917,12 @@ pub fn MarkdownEditor(initial_content: Option) -> Element { doc_for_spawn.add_image(&image, Some(&strong_ref.uri)); // Promote from pending to uploaded in resolver + let ident = AtIdentifier::Did(did); image_resolver.with_mut(|resolver| { resolver.promote_to_uploaded( &name_for_resolver, - cid, - did, - format, + blob_rkey, + ident, ); }); diff --git a/crates/weaver-app/src/components/editor/document.rs b/crates/weaver-app/src/components/editor/document.rs index ce7a4c9..c3840f7 100644 --- a/crates/weaver-app/src/components/editor/document.rs +++ b/crates/weaver-app/src/components/editor/document.rs @@ -10,19 +10,23 @@ //! - Content changes (via `last_edit`) trigger paragraph memo re-evaluation //! - The document struct itself is NOT wrapped in a Signal - use `use_hook` +use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; use dioxus::prelude::*; use loro::{ - ExportMode, LoroDoc, LoroList, LoroMap, LoroResult, LoroText, LoroValue, ToJson, UndoManager, + ExportMode, Frontiers, LoroDoc, LoroList, LoroMap, LoroResult, LoroText, LoroValue, ToJson, + UndoManager, VersionVector, cursor::{Cursor, Side}, }; use jacquard::IntoStatic; use jacquard::from_json_value; use jacquard::types::string::AtUri; +use weaver_api::com_atproto::repo::strong_ref::StrongRef; use weaver_api::sh_weaver::embed::images::Image; +use weaver_api::sh_weaver::notebook::entry::Entry; /// Helper for working with editor images. /// Constructed from LoroMap data, NOT serialized directly. @@ -80,10 +84,26 @@ pub struct EditorDocument { /// Contains nested containers: images (LoroList), externals (LoroList), etc. embeds: LoroMap, - // --- Entry tracking --- - /// AT-URI of the entry if editing an existing record. + // --- Entry tracking (reactive) --- + /// StrongRef to the entry if editing an existing record. /// None for new entries that haven't been published yet. - entry_uri: Option>, + /// Signal so cloned docs share the same state after publish. + pub entry_ref: Signal>>, + + // --- Edit sync state (for PDS sync) --- + /// StrongRef to the sh.weaver.edit.root record for this edit session. + /// None if we haven't synced to PDS yet. + pub edit_root: Signal>>, + + /// StrongRef to the most recent sh.weaver.edit.diff record. + /// Used for the `prev` field when creating new diffs. + /// None if no diffs have been created yet (only root exists). + pub last_diff: Signal>>, + + /// Version vector at the time of last sync to PDS. + /// Used to export only changes since last sync. + /// None if never synced. + last_synced_version: Option, // --- Editor state (non-reactive) --- /// Undo manager for the document. @@ -244,7 +264,10 @@ impl EditorDocument { created_at, tags, embeds, - entry_uri: None, + entry_ref: Signal::new(None), + edit_root: Signal::new(None), + last_diff: Signal::new(None), + last_synced_version: None, undo_mgr: Rc::new(RefCell::new(undo_mgr)), loro_cursor, // Reactive editor state - wrapped in Signals @@ -260,6 +283,44 @@ impl EditorDocument { } } + /// Create an EditorDocument from a fetched Entry. + /// + /// MUST be called from within a reactive context (e.g., `use_hook`) to + /// properly initialize Dioxus Signals. + /// + /// # Arguments + /// * `entry` - The entry record fetched from PDS + /// * `entry_ref` - StrongRef to the entry (URI + CID) + pub fn from_entry(entry: &Entry<'_>, entry_ref: StrongRef<'static>) -> Self { + let mut doc = Self::new(entry.content.to_string()); + + // Set metadata + doc.set_title(&entry.title); + doc.set_path(&entry.path); + doc.set_created_at(&entry.created_at.to_string()); + + // Add tags + if let Some(ref tags) = entry.tags { + for tag in tags.iter() { + doc.add_tag(tag.as_ref()); + } + } + + // Add existing images (no published_blob_uri needed - they're already in the entry) + if let Some(ref embeds) = entry.embeds { + if let Some(ref images) = embeds.images { + for img in &images.images { + doc.add_image(&img.clone().into_static(), None); + } + } + } + + // Set the entry_ref so subsequent publishes update this record + doc.set_entry_ref(Some(entry_ref)); + + doc + } + /// Generate current datetime as ISO 8601 string. #[cfg(target_family = "wasm")] fn current_datetime_string() -> String { @@ -359,16 +420,16 @@ impl EditorDocument { self.created_at.insert(0, datetime).ok(); } - // --- Entry URI accessors --- + // --- Entry ref accessors --- - /// Get the AT-URI of the entry if editing an existing record. - pub fn entry_uri(&self) -> Option<&AtUri<'static>> { - self.entry_uri.as_ref() + /// Get the StrongRef to the entry if editing an existing record. + pub fn entry_ref(&self) -> Option> { + self.entry_ref.read().clone() } - /// Set the AT-URI when editing an existing entry. - pub fn set_entry_uri(&mut self, uri: Option>) { - self.entry_uri = uri; + /// Set the StrongRef when editing an existing entry. + pub fn set_entry_ref(&mut self, entry: Option>) { + self.entry_ref.set(entry); } // --- Tags accessors --- @@ -689,16 +750,104 @@ impl EditorDocument { /// Get the current state frontiers for change detection. /// Frontiers represent the "version" of the document state. - pub fn state_frontiers(&self) -> loro::Frontiers { + pub fn state_frontiers(&self) -> Frontiers { self.doc.state_frontiers() } + /// Get the current version vector. + pub fn version_vector(&self) -> VersionVector { + self.doc.oplog_vv() + } + /// Get the last edit info for incremental rendering. /// Reading this creates a reactive dependency on content changes. pub fn last_edit(&self) -> Option { self.last_edit.read().clone() } + // --- Edit sync methods --- + + /// Get the edit root StrongRef if set. + pub fn edit_root(&self) -> Option> { + self.edit_root.read().clone() + } + + /// Set the edit root after creating or finding the root record. + pub fn set_edit_root(&mut self, root: Option>) { + self.edit_root.set(root); + } + + /// Get the last diff StrongRef if set. + pub fn last_diff(&self) -> Option> { + self.last_diff.read().clone() + } + + /// Set the last diff after creating a new diff record. + pub fn set_last_diff(&mut self, diff: Option>) { + self.last_diff.set(diff); + } + + /// Check if there are unsynchronized changes since the last sync. + pub fn has_unsync_changes(&self) -> bool { + match &self.last_synced_version { + Some(synced_vv) => self.doc.oplog_vv() != *synced_vv, + None => true, // Never synced, so there are changes + } + } + + /// Export updates since the last sync. + /// Returns None if there are no changes to export. + /// After successful upload, call `mark_synced()` to update the sync marker. + pub fn export_updates_since_sync(&self) -> Option> { + let from_vv = self.last_synced_version.clone().unwrap_or_default(); + let current_vv = self.doc.oplog_vv(); + + // No changes since last sync + if from_vv == current_vv { + return None; + } + + let updates = self + .doc + .export(ExportMode::Updates { + from: Cow::Owned(from_vv), + }) + .ok()?; + + // Don't return empty updates + if updates.is_empty() { + return None; + } + + Some(updates) + } + + /// Mark the current state as synced. + /// Call this after successfully uploading a diff to the PDS. + pub fn mark_synced(&mut self) { + self.last_synced_version = Some(self.doc.oplog_vv()); + } + + /// Import updates from a PDS diff blob. + /// Used when loading edit history from the PDS. + pub fn import_updates(&mut self, updates: &[u8]) -> LoroResult<()> { + self.doc.import(updates)?; + Ok(()) + } + + /// Set the sync state when loading from PDS. + /// This sets the version marker to the current state so we don't + /// re-upload what we just downloaded. + pub fn set_synced_from_pds( + &mut self, + edit_root: StrongRef<'static>, + last_diff: Option>, + ) { + self.edit_root.set(Some(edit_root)); + self.last_diff.set(last_diff); + self.last_synced_version = Some(self.doc.oplog_vv()); + } + /// Create a new EditorDocument from a binary snapshot. /// Falls back to empty document if import fails. /// @@ -765,7 +914,10 @@ impl EditorDocument { created_at, tags, embeds, - entry_uri: None, + entry_ref: Signal::new(None), + edit_root: Signal::new(None), + last_diff: Signal::new(None), + last_synced_version: None, undo_mgr: Rc::new(RefCell::new(undo_mgr)), loro_cursor, // Reactive editor state - wrapped in Signals diff --git a/crates/weaver-app/src/components/editor/input.rs b/crates/weaver-app/src/components/editor/input.rs index 6411d99..476a048 100644 --- a/crates/weaver-app/src/components/editor/input.rs +++ b/crates/weaver-app/src/components/editor/input.rs @@ -26,6 +26,10 @@ pub fn should_intercept_key(evt: &Event) -> bool { _ => {} } } + // Intercept Cmd+Backspace (delete to start of line) and Cmd+Delete (delete to end) + if matches!(key, Key::Backspace | Key::Delete) { + return true; + } // Let browser handle other Ctrl/Cmd shortcuts (paste, copy, cut, etc.) return false; } @@ -151,6 +155,17 @@ pub fn handle_keydown(evt: Event, doc: &mut EditorDocument) { doc.cursor.write().offset = start; } else if doc.cursor.read().offset > 0 { let cursor_offset = doc.cursor.read().offset; + + // Cmd+Backspace: delete to start of line + if mods.meta() || mods.ctrl() { + let line_start = find_line_start(doc.loro_text(), cursor_offset); + if line_start < cursor_offset { + let _ = doc.remove_tracked(line_start, cursor_offset - line_start); + doc.cursor.write().offset = line_start; + } + return; + } + // Check if we're about to delete a newline let prev_char = get_char_at(doc.loro_text(), cursor_offset - 1); @@ -213,7 +228,18 @@ pub fn handle_keydown(evt: Event, doc: &mut EditorDocument) { doc.cursor.write().offset = start; } else { let cursor_offset = doc.cursor.read().offset; - if cursor_offset < doc.len_chars() { + let doc_len = doc.len_chars(); + + // Cmd+Delete: delete to end of line + if mods.meta() || mods.ctrl() { + let line_end = find_line_end(doc.loro_text(), cursor_offset); + if cursor_offset < line_end { + let _ = doc.remove_tracked(cursor_offset, line_end - cursor_offset); + } + return; + } + + if cursor_offset < doc_len { // Delete next char let _ = doc.remove_tracked(cursor_offset, 1); } diff --git a/crates/weaver-app/src/components/editor/mod.rs b/crates/weaver-app/src/components/editor/mod.rs index 9fd2649..e5dad24 100644 --- a/crates/weaver-app/src/components/editor/mod.rs +++ b/crates/weaver-app/src/components/editor/mod.rs @@ -19,6 +19,7 @@ mod publish; mod render; mod report; mod storage; +mod sync; mod toolbar; mod visibility; mod writer; diff --git a/crates/weaver-app/src/components/editor/publish.rs b/crates/weaver-app/src/components/editor/publish.rs index 85d5452..9a8c0fc 100644 --- a/crates/weaver-app/src/components/editor/publish.rs +++ b/crates/weaver-app/src/components/editor/publish.rs @@ -1,11 +1,18 @@ -//! Entry publishing functionality for the markdown editor. +//! Entry publishing and loading functionality for the markdown editor. //! -//! Handles creating/updating AT Protocol notebook entries from editor state. +//! Handles creating/updating/loading AT Protocol notebook entries. use dioxus::prelude::*; +use jacquard::types::collection::Collection; use jacquard::types::ident::AtIdentifier; -use jacquard::types::string::{AtUri, Datetime, Nsid}; -use jacquard::{IntoStatic, prelude::*, to_data}; +use jacquard::types::recordkey::RecordKey; +use jacquard::types::string::{AtUri, Datetime, Nsid, Rkey}; +use jacquard::types::tid::Ticker; +use jacquard::{IntoStatic, from_data, prelude::*, to_data}; +use regex_lite::Regex; +use std::sync::LazyLock; +use weaver_api::com_atproto::repo::get_record::GetRecord; +use weaver_api::com_atproto::repo::strong_ref::StrongRef; use weaver_api::com_atproto::repo::{create_record::CreateRecord, put_record::PutRecord}; use weaver_api::sh_weaver::embed::images::Images; use weaver_api::sh_weaver::notebook::entry::{Entry, EntryEmbeds}; @@ -13,11 +20,29 @@ use weaver_common::{WeaverError, WeaverExt}; const ENTRY_NSID: &str = "sh.weaver.notebook.entry"; +/// Regex to match draft image paths: /image/{did}/draft/{blob_rkey}/{name} +/// Captures: 1=did, 2=blob_rkey, 3=name +static DRAFT_IMAGE_PATH_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"/image/([^/]+)/draft/([^/]+)/([^)\s]+)").unwrap()); + +/// Rewrite draft image paths to published paths. +/// +/// Converts `/image/{did}/draft/{blob_rkey}/{name}` to `/image/{did}/{entry_rkey}/{name}` +fn rewrite_draft_paths(content: &str, entry_rkey: &str) -> String { + DRAFT_IMAGE_PATH_REGEX + .replace_all(content, |caps: ®ex_lite::Captures| { + let did = &caps[1]; + let name = &caps[3]; + format!("/image/{}/{}/{}", did, entry_rkey, name) + }) + .into_owned() +} + use crate::auth::AuthState; use crate::fetch::Fetcher; use super::document::EditorDocument; -use super::storage::delete_draft; +use super::storage::{delete_draft, save_to_storage}; /// Result of a publish operation. #[derive(Clone, Debug)] @@ -36,6 +61,92 @@ impl PublishResult { } } +/// Result of fetching an entry for editing. +/// Contains the entry data and URI, but NOT an EditorDocument. +/// The document must be created in a reactive context (use_hook) to properly initialize Signals. +#[derive(Clone, PartialEq)] +pub struct LoadedEntry { + pub entry: Entry<'static>, + pub entry_ref: StrongRef<'static>, +} + +/// Fetch an existing entry from the PDS for editing. +/// +/// Returns the entry data and URI. The caller should create an `EditorDocument` +/// from this data using `EditorDocument::from_entry()` inside a reactive context. +/// +/// # Arguments +/// * `fetcher` - The fetcher for making API calls +/// * `uri` - The AT-URI of the entry to load (e.g., `at://did:plc:xxx/sh.weaver.notebook.entry/rkey`) +/// +/// # Returns +/// The entry and its URI, or an error. +pub async fn load_entry_for_editing( + fetcher: &Fetcher, + uri: &AtUri<'_>, +) -> Result { + // Parse the AT-URI components + let ident = uri.authority(); + let rkey = uri + .rkey() + .ok_or_else(|| WeaverError::InvalidNotebook("Entry URI missing rkey".into()))?; + + // Resolve DID and PDS + let (did, pds_url) = match ident { + AtIdentifier::Did(d) => { + let pds = fetcher + .client + .pds_for_did(d) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to resolve DID: {}", e)))?; + (d.clone(), pds) + } + AtIdentifier::Handle(h) => { + let (did, pds) = fetcher + .client + .pds_for_handle(h) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to resolve handle: {}", e)))?; + (did, pds) + } + }; + + // Fetch the entry record + let request = GetRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(Nsid::raw(::NSID)) + .rkey(rkey.clone()) + .build(); + + let response = fetcher + .client + .xrpc(pds_url) + .send(&request) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to fetch entry: {}", e)))?; + + let record = response + .into_output() + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to parse response: {}", e)))?; + + // Deserialize the entry + let entry: Entry = from_data(&record.value) + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to deserialize entry: {}", e)))?; + + // Build StrongRef from URI and CID + let entry_ref = StrongRef::new() + .uri(uri.clone().into_static()) + .cid(record.cid.ok_or_else(|| { + WeaverError::InvalidNotebook("Entry response missing CID".into()) + })?.into_static()) + .build(); + + Ok(LoadedEntry { + entry: entry.into_static(), + entry_ref, + }) +} + /// Publish an entry to the AT Protocol. /// /// Supports three modes: @@ -43,9 +154,14 @@ impl PublishResult { /// - Without notebook but with entry_uri in doc: uses `put_record` to update existing /// - Without notebook and no entry_uri: uses `create_record` for free-floating entry /// +/// Draft image paths (`/image/{did}/draft/{blob_rkey}/{name}`) are rewritten to +/// published paths (`/image/{did}/{entry_rkey}/{name}`) before publishing. +/// +/// On successful create, sets `doc.entry_uri` so subsequent publishes update the same record. +/// /// # Arguments /// * `fetcher` - The authenticated fetcher/client -/// * `doc` - The editor document containing entry data +/// * `doc` - The editor document containing entry data (mutable to update entry_uri) /// * `notebook_title` - Optional title of the notebook to publish to /// * `draft_key` - Storage key for the draft (for cleanup) /// @@ -53,7 +169,7 @@ impl PublishResult { /// The AT-URI of the created/updated entry, or an error. pub async fn publish_entry( fetcher: &Fetcher, - doc: &EditorDocument, + doc: &mut EditorDocument, notebook_title: Option<&str>, draft_key: &str, ) -> Result { @@ -96,38 +212,56 @@ pub async fn publish_entry( } }; - // Build the entry - let entry = Entry::new() - .content(doc.content()) - .title(doc.title()) - .path(path) - .created_at(Datetime::now()) - .maybe_tags(tags) - .maybe_embeds(entry_embeds) - .build(); - let entry_data = to_data(&entry).unwrap(); - let client = fetcher.get_client(); let result = if let Some(notebook) = notebook_title { // Publish to a notebook via upsert_entry - let (uri, was_created) = client.upsert_entry(notebook, &doc.title(), entry).await?; + // TODO: Need to handle path rewriting for notebook case + // For now, use content as-is (notebook entries use different path scheme anyway) + let entry = Entry::new() + .content(doc.content()) + .title(doc.title()) + .path(path) + .created_at(Datetime::now()) + .maybe_tags(tags) + .maybe_embeds(entry_embeds) + .build(); + + let (entry_ref, was_created) = client.upsert_entry(notebook, &doc.title(), entry).await?; + let uri = entry_ref.uri.clone(); + + // Set entry_ref so subsequent publishes update this record + doc.set_entry_ref(Some(entry_ref)); if was_created { PublishResult::Created(uri) } else { PublishResult::Updated(uri) } - } else if let Some(existing_uri) = doc.entry_uri() { - // Update existing free-floating entry + } else if let Some(existing_ref) = doc.entry_ref() { + // Update existing free-floating entry - use existing rkey for path rewriting let did = fetcher .current_did() .await .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; - let rkey = existing_uri + let rkey = existing_ref + .uri .rkey() .ok_or_else(|| WeaverError::InvalidNotebook("Entry URI missing rkey".into()))?; + // Rewrite draft image paths to published paths + let content = rewrite_draft_paths(&doc.content(), rkey.0.as_str()); + + let entry = Entry::new() + .content(content) + .title(doc.title()) + .path(path) + .created_at(Datetime::now()) + .maybe_tags(tags) + .maybe_embeds(entry_embeds) + .build(); + let entry_data = to_data(&entry).unwrap(); + let collection = Nsid::new(ENTRY_NSID).map_err(|e| WeaverError::AtprotoString(e))?; let request = PutRecord::new() @@ -145,19 +279,46 @@ pub async fn publish_entry( .into_output() .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; + // Update entry_ref with new CID + let updated_ref = StrongRef::new() + .uri(output.uri.clone().into_static()) + .cid(output.cid.into_static()) + .build(); + doc.set_entry_ref(Some(updated_ref)); + PublishResult::Updated(output.uri.into_static()) } else { - // Create new free-floating entry + // Create new free-floating entry - pre-generate rkey for path rewriting let did = fetcher .current_did() .await .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; + // Pre-generate TID for the entry rkey + let entry_tid = Ticker::new().next(None); + let entry_rkey_str = entry_tid.as_str(); + + // Rewrite draft image paths to published paths + let content = rewrite_draft_paths(&doc.content(), entry_rkey_str); + + let entry = Entry::new() + .content(content) + .title(doc.title()) + .path(path) + .created_at(Datetime::now()) + .maybe_tags(tags) + .maybe_embeds(entry_embeds) + .build(); + let entry_data = to_data(&entry).unwrap(); + let collection = Nsid::new(ENTRY_NSID).map_err(|e| WeaverError::AtprotoString(e))?; + let rkey = RecordKey::any(entry_rkey_str) + .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; let request = CreateRecord::new() .repo(AtIdentifier::Did(did)) .collection(collection) + .rkey(rkey) .record(entry_data) .build(); @@ -169,7 +330,14 @@ pub async fn publish_entry( .into_output() .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; - PublishResult::Created(output.uri.into_static()) + let uri = output.uri.into_static(); + // Set entry_ref so subsequent publishes update this record + let entry_ref = StrongRef::new() + .uri(uri.clone()) + .cid(output.cid.into_static()) + .build(); + doc.set_entry_ref(Some(entry_ref)); + PublishResult::Created(uri) }; // Cleanup: delete PublishedBlob records (entry's embed refs now keep blobs alive) @@ -180,9 +348,15 @@ pub async fn publish_entry( // } // } - // Clear local draft + // Delete the old draft key delete_draft(draft_key); + // Save with the new uri-based key so continued editing is tracked by entry URI + let new_key = result.uri().to_string(); + if let Err(e) = save_to_storage(doc, &new_key) { + tracing::warn!("Failed to save draft after publish: {e}"); + } + Ok(result) } @@ -237,7 +411,7 @@ pub fn PublishButton(props: PublishButtonProps) -> Element { let draft_key = props.draft_key.clone(); // Check if we're editing an existing entry - let is_editing_existing = doc.entry_uri().is_some(); + let is_editing_existing = doc.entry_ref().is_some(); // Validate that we have required fields let can_publish = !doc.title().trim().is_empty() && !doc.content().trim().is_empty(); @@ -268,7 +442,8 @@ pub fn PublishButton(props: PublishButtonProps) -> Element { is_publishing.set(true); error_message.set(None); - match publish_entry(&fetcher, &doc_snapshot, notebook.as_deref(), &draft_key).await { + let mut doc_snapshot = doc_snapshot; + match publish_entry(&fetcher, &mut doc_snapshot, notebook.as_deref(), &draft_key).await { Ok(result) => { success_uri.set(Some(result.uri().clone())); } diff --git a/crates/weaver-app/src/components/editor/storage.rs b/crates/weaver-app/src/components/editor/storage.rs index 75e9d90..39ca8a4 100644 --- a/crates/weaver-app/src/components/editor/storage.rs +++ b/crates/weaver-app/src/components/editor/storage.rs @@ -13,7 +13,8 @@ use dioxus::prelude::*; #[cfg(all(target_family = "wasm", target_os = "unknown"))] use gloo_storage::{LocalStorage, Storage}; use jacquard::IntoStatic; -use jacquard::types::string::AtUri; +use jacquard::types::string::{AtUri, Cid}; +use weaver_api::com_atproto::repo::strong_ref::StrongRef; use loro::cursor::Cursor; use serde::{Deserialize, Serialize}; @@ -58,6 +59,10 @@ pub struct EditorSnapshot { /// AT-URI if editing an existing entry (None for new entries) #[serde(default, skip_serializing_if = "Option::is_none")] pub editing_uri: Option, + + /// CID of the entry if editing an existing entry + #[serde(default, skip_serializing_if = "Option::is_none")] + pub editing_cid: Option, } /// Build the full storage key from a draft key. @@ -88,7 +93,8 @@ pub fn save_to_storage( snapshot: snapshot_b64, cursor: doc.loro_cursor().cloned(), cursor_offset: doc.cursor.read().offset, - editing_uri: doc.entry_uri().map(|u| u.to_string()), + editing_uri: doc.entry_ref().map(|r| r.uri.to_string()), + editing_cid: doc.entry_ref().map(|r| r.cid.to_string()), }; LocalStorage::set(storage_key(key), &snapshot) } @@ -104,12 +110,16 @@ pub fn save_to_storage( pub fn load_from_storage(key: &str) -> Option { let snapshot: EditorSnapshot = LocalStorage::get(storage_key(key)).ok()?; - // Parse entry_uri from the snapshot - let entry_uri = snapshot + // Parse entry_ref from the snapshot (requires both URI and CID) + let entry_ref = snapshot .editing_uri .as_ref() - .and_then(|s| AtUri::new(s).ok()) - .map(|u| u.into_static()); + .zip(snapshot.editing_cid.as_ref()) + .and_then(|(uri_str, cid_str)| { + let uri = AtUri::new(uri_str).ok()?.into_static(); + let cid = Cid::new(cid_str.as_bytes()).ok()?.into_static(); + Some(StrongRef::new().uri(uri).cid(cid).build()) + }); // Try to restore from CRDT snapshot first if let Some(ref snapshot_b64) = snapshot.snapshot { @@ -121,7 +131,7 @@ pub fn load_from_storage(key: &str) -> Option { ); // Verify the content matches (sanity check) if doc.content() == snapshot.content { - doc.set_entry_uri(entry_uri); + doc.set_entry_ref(entry_ref.clone()); return Some(doc); } tracing::warn!("Snapshot content mismatch, falling back to text content"); @@ -132,7 +142,7 @@ pub fn load_from_storage(key: &str) -> Option { let mut doc = EditorDocument::new(snapshot.content); doc.cursor.write().offset = snapshot.cursor_offset.min(doc.len_chars()); doc.sync_loro_cursor(); - doc.set_entry_uri(entry_uri); + doc.set_entry_ref(entry_ref); Some(doc) } diff --git a/crates/weaver-app/src/components/editor/sync.rs b/crates/weaver-app/src/components/editor/sync.rs new file mode 100644 index 0000000..15903c7 --- /dev/null +++ b/crates/weaver-app/src/components/editor/sync.rs @@ -0,0 +1,691 @@ +//! PDS synchronization for editor edit state. +//! +//! This module handles syncing the editor's Loro CRDT document to AT Protocol +//! edit records (`sh.weaver.edit.root` and `sh.weaver.edit.diff`). +//! +//! ## Edit State Structure +//! +//! - `sh.weaver.edit.root`: The starting point for an edit session, containing +//! a full Loro snapshot and a reference to the entry being edited. +//! - `sh.weaver.edit.diff`: Incremental updates since the root (or previous diff), +//! containing only the Loro delta bytes. +//! +//! ## Sync Flow +//! +//! 1. **First sync**: Create a root record with a full snapshot +//! 2. **Subsequent syncs**: Create diff records with deltas since last sync +//! 3. **Loading**: Find root via constellation backlinks, fetch all diffs, apply in order + +use std::collections::BTreeMap; + +use jacquard::cowstr::ToCowStr; +use jacquard::prelude::*; +use jacquard::types::blob::MimeType; +use jacquard::types::collection::Collection; +use jacquard::types::ident::AtIdentifier; +use jacquard::types::recordkey::RecordKey; +use jacquard::types::string::{AtUri, Cid, Did, Nsid}; +use jacquard::types::tid::Ticker; +use jacquard::types::uri::Uri; +use jacquard::url::Url; +use jacquard::{CowStr, IntoStatic, to_data}; +use weaver_api::com_atproto::repo::create_record::CreateRecord; +use weaver_api::com_atproto::repo::strong_ref::StrongRef; +use weaver_api::com_atproto::sync::get_blob::GetBlob; +use weaver_api::sh_weaver::edit::diff::Diff; +use weaver_api::sh_weaver::edit::root::Root; +use weaver_api::sh_weaver::edit::{DocRef, DocRefValue, DraftRef, EntryRef}; +use weaver_common::constellation::{GetBacklinksQuery, RecordId}; +use weaver_common::{WeaverError, WeaverExt}; + +use crate::fetch::Fetcher; + +use super::document::EditorDocument; + +const ROOT_NSID: &str = "sh.weaver.edit.root"; +const DIFF_NSID: &str = "sh.weaver.edit.diff"; +const CONSTELLATION_URL: &str = "https://constellation.microcosm.blue"; + +/// Build a DocRef for either a published entry or an unpublished draft. +/// +/// If entry_uri and entry_cid are provided, creates an EntryRef. +/// Otherwise, creates a DraftRef with the given draft key. +fn build_doc_ref( + draft_key: &str, + entry_uri: Option<&AtUri<'_>>, + entry_cid: Option<&Cid<'_>>, +) -> DocRef<'static> { + match (entry_uri, entry_cid) { + (Some(uri), Some(cid)) => DocRef { + value: DocRefValue::EntryRef(Box::new(EntryRef { + entry: StrongRef::new() + .uri(uri.clone().into_static()) + .cid(cid.clone().into_static()) + .build(), + extra_data: None, + })), + extra_data: None, + }, + _ => DocRef { + value: DocRefValue::DraftRef(Box::new(DraftRef { + draft_key: CowStr::from(draft_key.to_string()), + extra_data: None, + })), + extra_data: None, + }, + } +} + +/// Result of a sync operation. +#[derive(Clone, Debug)] +pub enum SyncResult { + /// Created a new root record (first sync) + CreatedRoot { + uri: AtUri<'static>, + cid: Cid<'static>, + }, + /// Created a new diff record + CreatedDiff { + uri: AtUri<'static>, + cid: Cid<'static>, + }, + /// No changes to sync + NoChanges, +} + +/// Find the edit root for an entry using constellation backlinks. +/// +/// Queries constellation for `sh.weaver.edit.root` records that reference +/// the given entry URI via the `.doc.value.entry.uri` path. +#[allow(dead_code)] +pub async fn find_edit_root_for_entry( + fetcher: &Fetcher, + entry_uri: &AtUri<'_>, +) -> Result>, WeaverError> { + let constellation_url = Url::parse(CONSTELLATION_URL) + .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid constellation URL: {}", e)))?; + + let query = GetBacklinksQuery { + subject: Uri::At(entry_uri.clone().into_static()), + source: format!("{}:.doc.value.entry.uri", ROOT_NSID).into(), + cursor: None, + did: vec![], + limit: 1, + }; + + let response = fetcher + .client + .xrpc(constellation_url) + .send(&query) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Constellation query failed: {}", e)))?; + + let output = response.into_output().map_err(|e| { + WeaverError::InvalidNotebook(format!("Failed to parse constellation response: {}", e)) + })?; + + Ok(output.records.into_iter().next().map(|r| r.into_static())) +} + +/// Find all diffs for a root record using constellation backlinks. +#[allow(dead_code)] +pub async fn find_diffs_for_root( + fetcher: &Fetcher, + root_uri: &AtUri<'_>, +) -> Result>, WeaverError> { + let constellation_url = Url::parse(CONSTELLATION_URL) + .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid constellation URL: {}", e)))?; + + let mut all_diffs = Vec::new(); + let mut cursor: Option = None; + + loop { + let query = GetBacklinksQuery { + subject: Uri::At(root_uri.clone().into_static()), + source: format!("{}:.root.uri", DIFF_NSID).into(), + cursor: cursor.map(Into::into), + did: vec![], + limit: 100, + }; + + let response = fetcher + .client + .xrpc(constellation_url.clone()) + .send(&query) + .await + .map_err(|e| { + WeaverError::InvalidNotebook(format!("Constellation query failed: {}", e)) + })?; + + let output = response.into_output().map_err(|e| { + WeaverError::InvalidNotebook(format!("Failed to parse constellation response: {}", e)) + })?; + + all_diffs.extend(output.records.into_iter().map(|r| r.into_static())); + + match output.cursor { + Some(c) => cursor = Some(c.to_string()), + None => break, + } + } + + Ok(all_diffs) +} + +/// Create the edit root record for an entry. +/// +/// Uploads the current Loro snapshot as a blob and creates an `sh.weaver.edit.root` +/// record referencing the entry (or draft key if unpublished). +/// +/// # Arguments +/// * `fetcher` - The authenticated fetcher +/// * `doc` - The editor document +/// * `draft_key` - The draft key (used for unpublished entries) +/// * `entry_uri` - Optional AT-URI of the published entry +/// * `entry_cid` - Optional CID of the published entry +pub async fn create_edit_root( + fetcher: &Fetcher, + doc: &EditorDocument, + draft_key: &str, + entry_uri: Option<&AtUri<'_>>, + entry_cid: Option<&Cid<'_>>, +) -> Result<(AtUri<'static>, Cid<'static>), WeaverError> { + let client = fetcher.get_client(); + let did = fetcher + .current_did() + .await + .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; + + // Export full snapshot + let snapshot = doc.export_snapshot(); + + // Upload snapshot blob + let mime_type = MimeType::new_static("application/octet-stream"); + let blob_ref = client + .upload_blob(snapshot, mime_type) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to upload snapshot: {}", e)))?; + + // Build DocRef - use EntryRef if published, DraftRef if not + let doc_ref = build_doc_ref(draft_key, entry_uri, entry_cid); + + // Build root record + let root = Root::new().doc(doc_ref).snapshot(blob_ref).build(); + + let root_data = to_data(&root) + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to serialize root: {}", e)))?; + + // Generate TID for the root rkey + let root_tid = Ticker::new().next(None); + let rkey = RecordKey::any(root_tid.as_str()) + .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; + + let collection = Nsid::new(ROOT_NSID).map_err(|e| WeaverError::AtprotoString(e))?; + + let request = CreateRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(collection) + .rkey(rkey) + .record(root_data) + .build(); + + let response = fetcher + .send(request) + .await + .map_err(jacquard::client::AgentError::from)?; + + let output = response + .into_output() + .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; + + Ok((output.uri.into_static(), output.cid.into_static())) +} + +/// Create a diff record with updates since the last sync. +/// +/// # Arguments +/// * `fetcher` - The authenticated fetcher +/// * `doc` - The editor document +/// * `root_uri` - URI of the edit root +/// * `root_cid` - CID of the edit root +/// * `prev_diff` - Optional reference to the previous diff +/// * `draft_key` - The draft key (used for doc reference) +/// * `entry_uri` - Optional AT-URI of the published entry +/// * `entry_cid` - Optional CID of the published entry +pub async fn create_diff( + fetcher: &Fetcher, + doc: &EditorDocument, + root_uri: &AtUri<'_>, + root_cid: &Cid<'_>, + prev_diff: Option<(&AtUri<'_>, &Cid<'_>)>, + draft_key: &str, + entry_uri: Option<&AtUri<'_>>, + entry_cid: Option<&Cid<'_>>, +) -> Result, Cid<'static>)>, WeaverError> { + // Export updates since last sync + let updates = match doc.export_updates_since_sync() { + Some(u) => u, + None => return Ok(None), // No changes + }; + + let client = fetcher.get_client(); + let did = fetcher + .current_did() + .await + .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; + + // Upload updates blob + let mime_type = MimeType::new_static("application/octet-stream"); + let blob_ref = client + .upload_blob(updates, mime_type) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to upload diff: {}", e)))?; + + // Build DocRef - use EntryRef if published, DraftRef if not + let doc_ref = build_doc_ref(draft_key, entry_uri, entry_cid); + + // Build root reference + let root_ref = StrongRef::new() + .uri(root_uri.clone().into_static()) + .cid(root_cid.clone().into_static()) + .build(); + + // Build prev reference if we have a previous diff + let prev_ref = prev_diff.map(|(uri, cid)| { + StrongRef::new() + .uri(uri.clone().into_static()) + .cid(cid.clone().into_static()) + .build() + }); + + // Build diff record + let diff = Diff::new() + .doc(doc_ref) + .root(root_ref) + .snapshot(blob_ref) + .maybe_prev(prev_ref) + .build(); + + let diff_data = to_data(&diff) + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to serialize diff: {}", e)))?; + + // Generate TID for the diff rkey + let diff_tid = Ticker::new().next(None); + let rkey = RecordKey::any(diff_tid.as_str()) + .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; + + let collection = Nsid::new(DIFF_NSID).map_err(|e| WeaverError::AtprotoString(e))?; + + let request = CreateRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(collection) + .rkey(rkey) + .record(diff_data) + .build(); + + let response = fetcher + .send(request) + .await + .map_err(jacquard::client::AgentError::from)?; + + let output = response + .into_output() + .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; + + Ok(Some((output.uri.into_static(), output.cid.into_static()))) +} + +/// Sync the document to the PDS. +/// +/// If no edit root exists, creates one with a full snapshot. +/// If a root exists, creates a diff with updates since last sync. +/// +/// Updates the document's sync state on success. +/// +/// # Arguments +/// * `fetcher` - The authenticated fetcher +/// * `doc` - The editor document (mutable to update sync state) +/// * `draft_key` - The draft key for this document +/// +/// # Returns +/// The sync result indicating what was created. +pub async fn sync_to_pds( + fetcher: &Fetcher, + doc: &mut EditorDocument, + draft_key: &str, +) -> Result { + // Check if we have changes to sync + if !doc.has_unsync_changes() { + return Ok(SyncResult::NoChanges); + } + + // Get entry info if published + let entry_ref = doc.entry_ref(); + + if doc.edit_root().is_none() { + // First sync - create root + let (root_uri, root_cid) = create_edit_root( + fetcher, + doc, + draft_key, + entry_ref.as_ref().map(|r| &r.uri), + entry_ref.as_ref().map(|r| &r.cid), + ) + .await?; + + // Build StrongRef for the root + let root_ref = StrongRef::new() + .uri(root_uri.clone()) + .cid(root_cid.clone()) + .build(); + + // Update document state + doc.set_edit_root(Some(root_ref)); + doc.set_last_diff(None); + doc.mark_synced(); + + Ok(SyncResult::CreatedRoot { + uri: root_uri, + cid: root_cid, + }) + } else { + // Subsequent sync - create diff + let root_ref = doc.edit_root().unwrap(); + let prev_diff = doc.last_diff(); + + let result = create_diff( + fetcher, + doc, + &root_ref.uri, + &root_ref.cid, + prev_diff.as_ref().map(|d| (&d.uri, &d.cid)), + draft_key, + entry_ref.as_ref().map(|r| &r.uri), + entry_ref.as_ref().map(|r| &r.cid), + ) + .await?; + + match result { + Some((diff_uri, diff_cid)) => { + // Build StrongRef for the diff + let diff_ref = StrongRef::new() + .uri(diff_uri.clone()) + .cid(diff_cid.clone()) + .build(); + + doc.set_last_diff(Some(diff_ref)); + doc.mark_synced(); + + Ok(SyncResult::CreatedDiff { + uri: diff_uri, + cid: diff_cid, + }) + } + None => Ok(SyncResult::NoChanges), + } + } +} + +/// Result of loading edit state from PDS. +#[derive(Clone, Debug)] +pub struct PdsEditState { + /// The root record reference + pub root_ref: StrongRef<'static>, + /// The latest diff reference (if any diffs exist) + pub last_diff_ref: Option>, + /// The Loro snapshot bytes from the root + pub root_snapshot: Vec, + /// All diff update bytes in order (oldest first, by TID) + pub diff_updates: Vec>, +} + +/// Fetch a blob from the PDS. +async fn fetch_blob( + fetcher: &Fetcher, + did: &Did<'_>, + cid: &Cid<'_>, +) -> Result, WeaverError> { + let pds_url = fetcher + .client + .pds_for_did(did) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to resolve DID: {}", e)))?; + + let request = GetBlob::new().did(did.clone()).cid(cid.clone()).build(); + + let response = fetcher + .client + .xrpc(pds_url) + .send(&request) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to fetch blob: {}", e)))?; + + let output = response.into_output().map_err(|e| { + WeaverError::InvalidNotebook(format!("Failed to parse blob response: {}", e)) + })?; + + Ok(output.body.to_vec()) +} + +/// Load edit state from the PDS for an entry. +/// +/// Finds the edit root via constellation backlinks, fetches all diffs, +/// and returns the snapshot + updates needed to reconstruct the document. +/// +/// # Arguments +/// * `fetcher` - The authenticated fetcher +/// * `entry_uri` - The AT-URI of the entry to load edit state for +/// +/// # Returns +/// The edit state if found, or None if no edit root exists for this entry. +pub async fn load_edit_state_from_pds( + fetcher: &Fetcher, + entry_uri: &AtUri<'_>, +) -> Result, WeaverError> { + // Find the edit root for this entry + let root_id = match find_edit_root_for_entry(fetcher, entry_uri).await? { + Some(id) => id, + None => return Ok(None), + }; + + // Build root URI + let root_uri = AtUri::new(&format!( + "at://{}/{}/{}", + root_id.did(), + ROOT_NSID, + root_id.rkey().as_ref() + )) + .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid root URI: {}", e)))? + .into_static(); + + // Fetch the root record using get_record helper + let root_response = fetcher + .client + .get_record::(&root_uri) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to fetch root: {}", e)))?; + + let root_output = root_response + .into_output() + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to parse root: {}", e)))?; + + let root_cid = root_output + .cid + .ok_or_else(|| WeaverError::InvalidNotebook("Root response missing CID".into()))?; + + let root_ref = StrongRef::new() + .uri(root_uri.clone()) + .cid(root_cid.into_static()) + .build(); + + // Fetch the root snapshot blob + let root_snapshot = fetch_blob( + fetcher, + &root_id.did(), + root_output.value.snapshot.blob().cid(), + ) + .await?; + + // Find all diffs for this root + let diff_ids = find_diffs_for_root(fetcher, &root_uri).await?; + + if diff_ids.is_empty() { + return Ok(Some(PdsEditState { + root_ref, + last_diff_ref: None, + root_snapshot, + diff_updates: vec![], + })); + } + + // Fetch all diffs and store in BTreeMap keyed by rkey (TID) for sorted order + // TIDs are lexicographically sortable timestamps + let mut diffs_by_rkey: BTreeMap< + CowStr<'static>, + (Diff<'static>, Cid<'static>, AtUri<'static>), + > = BTreeMap::new(); + + for diff_id in &diff_ids { + let rkey = diff_id.rkey(); + let rkey_str: &str = rkey.as_ref(); + let diff_uri = AtUri::new(&format!( + "at://{}/{}/{}", + diff_id.did(), + DIFF_NSID, + rkey_str + )) + .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid diff URI: {}", e)))? + .into_static(); + + let diff_response = fetcher + .client + .get_record::(&diff_uri) + .await + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to fetch diff: {}", e)))?; + + let diff_output = diff_response + .into_output() + .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to parse diff: {}", e)))?; + + let diff_cid = diff_output + .cid + .ok_or_else(|| WeaverError::InvalidNotebook("Diff response missing CID".into()))?; + + diffs_by_rkey.insert( + rkey_str.to_cowstr().into_static(), + ( + diff_output.value.into_static(), + diff_cid.into_static(), + diff_uri, + ), + ); + } + + // Fetch all diff blobs in TID order (BTreeMap iterates in sorted order) + let mut diff_updates = Vec::new(); + let mut last_diff_ref = None; + + for (_rkey, (diff, cid, uri)) in &diffs_by_rkey { + let blob_bytes = fetch_blob(fetcher, &root_id.did(), diff.snapshot.blob().cid()).await?; + diff_updates.push(blob_bytes); + + // Track the last diff (will be the one with highest TID after iteration) + last_diff_ref = Some(StrongRef::new().uri(uri.clone()).cid(cid.clone()).build()); + } + + Ok(Some(PdsEditState { + root_ref, + last_diff_ref, + root_snapshot, + diff_updates, + })) +} + +/// Load an EditorDocument by merging local storage and PDS state. +/// +/// This is the main entry point for loading a document with full sync support. +/// It: +/// 1. Loads from localStorage (if available) +/// 2. Loads from PDS (if available) +/// 3. Merges both using Loro's CRDT merge +/// +/// The result is a document with all changes from both sources. +/// +/// # Arguments +/// * `fetcher` - The authenticated fetcher +/// * `draft_key` - The localStorage key for this draft +/// * `entry_uri` - Optional AT-URI if editing an existing entry +/// +/// # Returns +/// A merged EditorDocument, or None if no state exists anywhere. +pub async fn load_and_merge_document( + fetcher: &Fetcher, + draft_key: &str, + entry_uri: Option<&AtUri<'_>>, +) -> Result, WeaverError> { + use super::storage::load_from_storage; + + // Load from localStorage + let local_doc = load_from_storage(draft_key); + + // Load from PDS (only if we have an entry URI) + let pds_state = if let Some(uri) = entry_uri { + load_edit_state_from_pds(fetcher, uri).await? + } else { + None + }; + + match (local_doc, pds_state) { + (None, None) => Ok(None), + + (Some(doc), None) => { + // Only local state exists + tracing::debug!("Loaded document from localStorage only"); + Ok(Some(doc)) + } + + (None, Some(pds)) => { + // Only PDS state exists - reconstruct from snapshot + diffs + tracing::debug!("Loaded document from PDS only"); + let mut doc = EditorDocument::from_snapshot(&pds.root_snapshot, None, 0); + + // Apply all diffs in order + for updates in &pds.diff_updates { + if let Err(e) = doc.import_updates(updates) { + tracing::warn!("Failed to apply diff update: {:?}", e); + } + } + + // Set sync state so we don't re-upload what we just downloaded + doc.set_synced_from_pds(pds.root_ref, pds.last_diff_ref); + + Ok(Some(doc)) + } + + (Some(mut local_doc), Some(pds)) => { + // Both exist - merge using CRDT + tracing::debug!("Merging document from localStorage and PDS"); + + // Import PDS root snapshot into local doc + // Loro will automatically merge concurrent changes + if let Err(e) = local_doc.import_updates(&pds.root_snapshot) { + tracing::warn!("Failed to merge PDS root snapshot: {:?}", e); + } + + // Import all diffs + for updates in &pds.diff_updates { + if let Err(e) = local_doc.import_updates(updates) { + tracing::warn!("Failed to merge PDS diff: {:?}", e); + } + } + + // Update sync state + // We keep the PDS root/diff refs since that's where we'll push updates + local_doc.set_edit_root(Some(pds.root_ref)); + local_doc.set_last_diff(pds.last_diff_ref); + // Don't call set_synced_from_pds - local changes still need syncing + + Ok(Some(local_doc)) + } + } +} diff --git a/crates/weaver-app/src/components/editor/writer.rs b/crates/weaver-app/src/components/editor/writer.rs index 068b4bb..e5f22d0 100644 --- a/crates/weaver-app/src/components/editor/writer.rs +++ b/crates/weaver-app/src/components/editor/writer.rs @@ -7,6 +7,7 @@ //! represent consumed formatting characters. use super::offset_map::{OffsetMapping, RenderResult}; +use jacquard::types::{ident::AtIdentifier, string::Rkey}; use loro::LoroText; use markdown_weaver::{ Alignment, BlockQuoteKind, CodeBlockKind, CowStr, EmbedType, Event, LinkType, Tag, @@ -138,17 +139,35 @@ impl ImageResolver for () { /// Concrete image resolver that maps image names to URLs. /// -/// Supports two states for images: +/// Resolved image path type +#[derive(Clone, Debug)] +enum ResolvedImage { + /// Data URL for immediate preview (still uploading) + Pending(String), + /// Draft image: `/image/{ident}/draft/{blob_rkey}/{name}` + Draft { + blob_rkey: Rkey<'static>, + ident: AtIdentifier<'static>, + }, + /// Published image: `/image/{ident}/{entry_rkey}/{name}` + Published { + entry_rkey: Rkey<'static>, + ident: AtIdentifier<'static>, + }, +} + +/// Resolves image paths in the editor. +/// +/// Supports three states for images: /// - Pending: uses data URL for immediate preview while upload is in progress -/// - Uploaded: uses CDN URL format `https://cdn.bsky.app/img/feed_fullsize/plain/{did}/{cid}@{format}` +/// - Draft: uses path format `/image/{did}/draft/{blob_rkey}/{name}` +/// - Published: uses path format `/image/{did}/{entry_rkey}/{name}` /// /// Image URLs in markdown use the format `/image/{name}`. #[derive(Clone, Default)] pub struct EditorImageResolver { - /// Pending images: name -> data URL (still uploading) - pending: std::collections::HashMap, - /// Uploaded images: name -> (CID string, DID string, format) - uploaded: std::collections::HashMap, + /// All resolved images: name -> resolved path info + images: std::collections::HashMap, } impl EditorImageResolver { @@ -162,38 +181,81 @@ impl EditorImageResolver { /// * `name` - The image name used in markdown (e.g., "photo.jpg") /// * `data_url` - The base64 data URL for preview pub fn add_pending(&mut self, name: String, data_url: String) { - self.pending.insert(name, data_url); + self.images.insert(name, ResolvedImage::Pending(data_url)); + } + + /// Promote a pending image to uploaded (draft) status. + /// + /// # Arguments + /// * `name` - The image name used in markdown + /// * `blob_rkey` - The rkey of the PublishedBlob record + /// * `ident` - The AT identifier (DID or handle) of the blob owner + pub fn promote_to_uploaded( + &mut self, + name: &str, + blob_rkey: Rkey<'static>, + ident: AtIdentifier<'static>, + ) { + self.images.insert( + name.to_string(), + ResolvedImage::Draft { blob_rkey, ident }, + ); } - /// Promote a pending image to uploaded status. + /// Add an already-uploaded draft image. /// - /// Removes from pending and adds to uploaded with CDN info. - pub fn promote_to_uploaded(&mut self, name: &str, cid: String, did: String, format: String) { - self.pending.remove(name); - self.uploaded.insert(name.to_string(), (cid, did, format)); + /// # Arguments + /// * `name` - The name/URL used in markdown (e.g., "photo.jpg") + /// * `blob_rkey` - The rkey of the PublishedBlob record + /// * `ident` - The AT identifier (DID or handle) of the blob owner + pub fn add_uploaded( + &mut self, + name: String, + blob_rkey: Rkey<'static>, + ident: AtIdentifier<'static>, + ) { + self.images + .insert(name, ResolvedImage::Draft { blob_rkey, ident }); } - /// Add an already-uploaded image. + /// Add a published image. /// /// # Arguments /// * `name` - The name/URL used in markdown (e.g., "photo.jpg") - /// * `cid` - The blob CID - /// * `did` - The DID of the blob owner - /// * `format` - The image format (e.g., "jpeg", "png") - pub fn add_uploaded(&mut self, name: String, cid: String, did: String, format: String) { - self.uploaded.insert(name, (cid, did, format)); + /// * `entry_rkey` - The rkey of the entry record containing this image + /// * `ident` - The AT identifier (DID or handle) of the entry owner + pub fn add_published( + &mut self, + name: String, + entry_rkey: Rkey<'static>, + ident: AtIdentifier<'static>, + ) { + self.images + .insert(name, ResolvedImage::Published { entry_rkey, ident }); } /// Check if an image is pending upload. pub fn is_pending(&self, name: &str) -> bool { - self.pending.contains_key(name) + matches!(self.images.get(name), Some(ResolvedImage::Pending(_))) } - /// Build a resolver from editor images and user DID. + /// Build a resolver from editor images and user identifier. + /// + /// # Arguments + /// * `images` - Iterator of editor images + /// * `ident` - The AT identifier (DID or handle) of the user + /// * `entry_rkey` - If Some, images are resolved as published (`/image/{ident}/{entry_rkey}/{name}`). + /// If None, images are resolved as drafts using their `published_blob_uri`. + /// + /// For draft mode (entry_rkey=None), only images with a `published_blob_uri` are included. + /// For published mode (entry_rkey=Some), all images are included. pub fn from_images<'a>( images: impl IntoIterator, - user_did: &str, + ident: AtIdentifier<'static>, + entry_rkey: Option>, ) -> Self { + use jacquard::IntoStatic; + let mut resolver = Self::new(); for editor_image in images { // Get the name from the Image (use alt text as fallback if name is empty) @@ -208,17 +270,23 @@ impl EditorImageResolver { continue; } - // Get CID and format from the blob ref - let blob = editor_image.image.image.blob(); - let cid = blob.cid().to_string(); - let format = blob - .mime_type - .0 - .strip_prefix("image/") - .unwrap_or("jpeg") - .to_string(); - - resolver.add_uploaded(name, cid, user_did.to_string(), format); + match &entry_rkey { + // Published mode: use entry rkey for all images + Some(rkey) => { + resolver.add_published(name, rkey.clone(), ident.clone()); + } + // Draft mode: use published_blob_uri rkey + None => { + let blob_rkey = match &editor_image.published_blob_uri { + Some(uri) => match uri.rkey() { + Some(rkey) => rkey.0.clone().into_static(), + None => continue, + }, + None => continue, + }; + resolver.add_uploaded(name, blob_rkey, ident.clone()); + } + } } resolver } @@ -229,17 +297,16 @@ impl ImageResolver for EditorImageResolver { // Extract image name from /image/{name} format let name = url.strip_prefix("/image/").unwrap_or(url); - // Check pending first (data URL for immediate preview) - if let Some(data_url) = self.pending.get(name) { - return Some(data_url.clone()); + let resolved = self.images.get(name)?; + match resolved { + ResolvedImage::Pending(data_url) => Some(data_url.clone()), + ResolvedImage::Draft { blob_rkey, ident } => { + Some(format!("/image/{}/draft/{}/{}", ident, blob_rkey, name)) + } + ResolvedImage::Published { entry_rkey, ident } => { + Some(format!("/image/{}/{}/{}", ident, entry_rkey, name)) + } } - - // Then check uploaded (CDN URL) - let (cid, did, format) = self.uploaded.get(name)?; - Some(format!( - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", - did, cid, format - )) } } diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index c908468..2a64a0b 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -54,8 +54,8 @@ enum Route { #[layout(Navbar)] #[route("/")] Home {}, - #[route("/editor")] - Editor {}, + #[route("/editor?:entry")] + Editor { entry: Option }, #[layout(ErrorLayout)] #[nest("/record")] #[layout(RecordIndex)] @@ -304,12 +304,23 @@ pub async fn favicon() -> axum::response::Response { #[cfg(all(feature = "fullstack-server", feature = "server"))] #[get("/{notebook}/image/{name}", blob_cache: Extension>)] pub async fn image_named(notebook: SmolStr, name: SmolStr) -> Result { - use axum::{http::header::CONTENT_TYPE, response::IntoResponse}; + use axum::{ + http::header::{CACHE_CONTROL, CONTENT_TYPE}, + response::IntoResponse, + }; use mime_sniffer::MimeTypeSniffer; if let Some(bytes) = blob_cache.get_named(&name) { let blob = bytes.clone(); let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); - Ok(([(CONTENT_TYPE, mime)], bytes).into_response()) + // Blobs are immutable by CID - cache aggressively + Ok(( + [ + (CONTENT_TYPE, mime), + (CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ) + .into_response()) } else { Err(CapturedError::from_display("no image")) } @@ -318,17 +329,162 @@ pub async fn image_named(notebook: SmolStr, name: SmolStr) -> Result>)] pub async fn blob(notebook: SmolStr, cid: SmolStr) -> Result { - use axum::{http::header::CONTENT_TYPE, response::IntoResponse}; + use axum::{ + http::header::{CACHE_CONTROL, CONTENT_TYPE}, + response::IntoResponse, + }; use mime_sniffer::MimeTypeSniffer; if let Some(bytes) = blob_cache.get_cid(&Cid::new_owned(cid.as_bytes())?) { let blob = bytes.clone(); let mime = blob.sniff_mime_type().unwrap_or("application/octet-stream"); - Ok(([(CONTENT_TYPE, mime)], bytes).into_response()) + // Blobs are immutable by CID - cache aggressively + Ok(( + [ + (CONTENT_TYPE, mime), + (CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ) + .into_response()) } else { Err(CapturedError::from_display("no blob")) } } +// New image routes with unified /image/ prefix +// Route: /image/{notebook}/{name} - notebook entry image by name +#[cfg(all(feature = "fullstack-server", feature = "server"))] +#[get("/image/{notebook}/{name}", blob_cache: Extension>)] +pub async fn image_notebook(notebook: SmolStr, name: SmolStr) -> Result { + use axum::{ + http::header::{CACHE_CONTROL, CONTENT_TYPE}, + response::IntoResponse, + }; + use mime_sniffer::MimeTypeSniffer; + + // Try name-based lookup first (backwards compat with cached entries) + if let Some(bytes) = blob_cache.get_named(&name) { + let blob = bytes.clone(); + let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); + return Ok(( + [ + (CONTENT_TYPE, mime), + (CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ) + .into_response()); + } + + // Try to resolve from notebook + match blob_cache.resolve_from_notebook(¬ebook, &name).await { + Ok(bytes) => { + let blob = bytes.clone(); + let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); + Ok(( + [ + (CONTENT_TYPE, mime), + (CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ) + .into_response()) + } + Err(e) => Err(e), + } +} + +// Route: /image/{ident}/draft/{blob_rkey} - draft image (unpublished) +// Route: /image/{ident}/draft/{blob_rkey}/{name} - draft image with name +#[cfg(all(feature = "fullstack-server", feature = "server"))] +#[get("/image/{ident}/draft/{blob_rkey}", blob_cache: Extension>)] +pub async fn image_draft(ident: SmolStr, blob_rkey: SmolStr) -> Result { + use axum::{ + http::header::{CACHE_CONTROL, CONTENT_TYPE}, + response::IntoResponse, + }; + use mime_sniffer::MimeTypeSniffer; + + let at_ident = AtIdentifier::new_owned(ident.clone()) + .map_err(|e| CapturedError::from_display(format!("Invalid identifier '{}': {}", ident, e)))?; + + match blob_cache.resolve_from_draft(&at_ident, &blob_rkey).await { + Ok(bytes) => { + let blob = bytes.clone(); + let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); + Ok(( + [ + (CONTENT_TYPE, mime), + (CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ) + .into_response()) + } + Err(e) => Err(e), + } +} + +#[cfg(all(feature = "fullstack-server", feature = "server"))] +#[get("/image/{ident}/draft/{blob_rkey}/{name}", blob_cache: Extension>)] +pub async fn image_draft_named(ident: SmolStr, blob_rkey: SmolStr, name: SmolStr) -> Result { + use axum::{ + http::header::{CACHE_CONTROL, CONTENT_TYPE}, + response::IntoResponse, + }; + use mime_sniffer::MimeTypeSniffer; + + // Name is optional/decorative for drafts, just use the blob_rkey + let at_ident = AtIdentifier::new_owned(ident.clone()) + .map_err(|e| CapturedError::from_display(format!("Invalid identifier '{}': {}", ident, e)))?; + + match blob_cache.resolve_from_draft(&at_ident, &blob_rkey).await { + Ok(bytes) => { + let blob = bytes.clone(); + let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); + Ok(( + [ + (CONTENT_TYPE, mime), + (CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ) + .into_response()) + } + Err(e) => Err(e), + } +} + +// Route: /image/{ident}/{rkey}/{name} - published entry image +#[cfg(all(feature = "fullstack-server", feature = "server"))] +#[get("/image/{ident}/{rkey}/{name}", blob_cache: Extension>)] +pub async fn image_entry(ident: SmolStr, rkey: SmolStr, name: SmolStr) -> Result { + use axum::{ + http::header::{CACHE_CONTROL, CONTENT_TYPE}, + response::IntoResponse, + }; + use mime_sniffer::MimeTypeSniffer; + + let at_ident = AtIdentifier::new_owned(ident.clone()) + .map_err(|e| CapturedError::from_display(format!("Invalid identifier '{}': {}", ident, e)))?; + + match blob_cache.resolve_from_entry(&at_ident, &rkey, &name).await { + Ok(bytes) => { + let blob = bytes.clone(); + let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); + Ok(( + [ + (CONTENT_TYPE, mime), + (CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ) + .into_response()) + } + Err(e) => Err(e), + } +} + // #[server(endpoint = "static_routes", output = server_fn::codec::Json)] // async fn static_routes() -> Result, ServerFnError> { // // The `Routable` trait has a `static_routes` method that returns all static routes in the enum diff --git a/crates/weaver-app/src/views/editor.rs b/crates/weaver-app/src/views/editor.rs index 0d78480..95522b1 100644 --- a/crates/weaver-app/src/views/editor.rs +++ b/crates/weaver-app/src/views/editor.rs @@ -5,14 +5,14 @@ use dioxus::prelude::*; /// Editor page view. /// -/// Displays the markdown editor at the /editor route for testing during development. -/// Eventually this will be integrated into the notebook editing workflow. +/// Displays the markdown editor at the /editor route. +/// Optionally loads an existing entry for editing via `?entry={at-uri}`. #[component] -pub fn Editor() -> Element { +pub fn Editor(entry: Option) -> Element { rsx! { EditorCss {} div { class: "editor-page", - MarkdownEditor { initial_content: None } + MarkdownEditor { entry_uri: entry } } } } diff --git a/crates/weaver-common/src/agent.rs b/crates/weaver-common/src/agent.rs index 5658a73..433dcab 100644 --- a/crates/weaver-common/src/agent.rs +++ b/crates/weaver-common/src/agent.rs @@ -216,13 +216,13 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { /// 3. If found: update the entry with new content /// 4. If not found: create new entry and append to notebook's entry_list /// - /// Returns (entry_uri, was_created) + /// Returns (entry_ref, was_created) fn upsert_entry( &self, notebook_title: &str, entry_title: &str, entry: entry::Entry<'_>, - ) -> impl Future, bool), WeaverError>> + ) -> impl Future, bool), WeaverError>> where Self: Sized, { @@ -244,31 +244,42 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { if let Ok(existing_entry) = existing.parse() { if existing_entry.value.title == entry_title { // Update existing entry - self.update_record::(&entry_ref.uri, |e| { - e.content = entry.content.clone(); - e.embeds = entry.embeds.clone(); - e.tags = entry.tags.clone(); - }) - .await?; - return Ok((entry_ref.uri.clone().into_static(), false)); + let output = self + .update_record::(&entry_ref.uri, |e| { + e.content = entry.content.clone(); + e.embeds = entry.embeds.clone(); + e.tags = entry.tags.clone(); + }) + .await?; + let updated_ref = StrongRef::new() + .uri(output.uri.into_static()) + .cid(output.cid.into_static()) + .build(); + return Ok((updated_ref, false)); } } } // Entry doesn't exist, create it let response = self.create_record(entry, None).await?; - let entry_uri = response.uri.clone(); + let new_ref = StrongRef::new() + .uri(response.uri.clone().into_static()) + .cid(response.cid.clone().into_static()) + .build(); // Add to notebook's entry_list use weaver_api::sh_weaver::notebook::book::Book; - let new_ref = StrongRef::new().uri(response.uri).cid(response.cid).build(); + let notebook_entry_ref = StrongRef::new() + .uri(response.uri.into_static()) + .cid(response.cid.into_static()) + .build(); self.update_record::(¬ebook_uri, |book| { - book.entry_list.push(new_ref); + book.entry_list.push(notebook_entry_ref); }) .await?; - Ok((entry_uri, true)) + Ok((new_ref, true)) } } diff --git a/crates/weaver-common/src/constellation.rs b/crates/weaver-common/src/constellation.rs index cd52245..13f13e4 100644 --- a/crates/weaver-common/src/constellation.rs +++ b/crates/weaver-common/src/constellation.rs @@ -26,29 +26,29 @@ pub struct GetBacklinksQuery<'a> { /// The link target /// /// can be an AT-URI, plain DID, or regular URI - subject: jacquard::types::uri::Uri<'a>, + pub subject: jacquard::types::uri::Uri<'a>, /// Filter links only from this link source /// /// eg.: `app.bsky.feed.like:subject.uri` - source: CowStr<'a>, + pub source: CowStr<'a>, #[serde(borrow)] - cursor: Option>, + pub cursor: Option>, /// Filter links only from these DIDs /// /// include multiple times to filter by multiple source DIDs #[serde(default)] - did: Vec>, + pub did: Vec>, /// Set the max number of links to return per page of results #[serde(default = "get_default_cursor_limit")] - limit: u64, + pub limit: u64, // TODO: allow reverse (er, forward) order as well } #[derive(Deserialize, Serialize, IntoStatic)] pub struct GetBacklinksResponse<'a> { - total: u64, + pub total: u64, #[serde(borrow)] - records: Vec>, - cursor: Option>, + pub records: Vec>, + pub cursor: Option>, } #[derive(Debug, PartialEq, Serialize, Deserialize, IntoStatic)] diff --git a/lexicons.kdl b/lexicons.kdl index 646e06c..69c2fb0 100644 --- a/lexicons.kdl +++ b/lexicons.kdl @@ -5,11 +5,18 @@ output { } -source "bluesky" type="git" priority=101 { - repo "https://github.com/bluesky-social/atproto" - pattern "**/*.json" +// source "bluesky" type="git" priority=101 { +// repo "https://github.com/bluesky-social/atproto" +// pattern "**/*.json" +// } + +source "weaver" type="local" priority=100 { + //path "./lexicons" + path "/home/orual/Projects/weaver.sh/crates/weaver-api/lexicons" + } + source "weaver" type="local" priority=200 { path "./lexicons" diff --git a/lexicons/edit/defs.json b/lexicons/edit/defs.json index 9a8fd9c..1ceba81 100644 --- a/lexicons/edit/defs.json +++ b/lexicons/edit/defs.json @@ -8,7 +8,7 @@ "properties": { "value": { "type": "union", - "refs": ["#notebookRef", "#entryRef"] + "refs": ["#notebookRef", "#entryRef", "#draftRef"] } } }, @@ -24,13 +24,24 @@ }, "entryRef": { "type": "object", - "required": ["notebook"], + "required": ["entry"], "properties": { - "notebook": { + "entry": { "type": "ref", "ref": "com.atproto.repo.strongRef" } } + }, + + "draftRef": { + "type": "object", + "required": ["draft_key"], + "properties": { + "draft_key": { + "type": "string", + "maxLength": 200 + } + } } } } diff --git a/lexicons/edit/diff.json b/lexicons/edit/diff.json index 9cd13ee..4d53b91 100644 --- a/lexicons/edit/diff.json +++ b/lexicons/edit/diff.json @@ -19,6 +19,10 @@ "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "prev": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, "doc": { "type": "ref", "ref": "sh.weaver.edit.defs#docRef"