1#[allow(unused_imports)]
9use alloc::collections::BTreeMap;
10
11#[allow(unused_imports)]
12use core::marker::PhantomData;
13use jacquard_common::CowStr;
14
15#[allow(unused_imports)]
16use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation;
17use jacquard_common::types::blob::BlobRef;
18use jacquard_common::types::collection::{Collection, RecordError};
19use jacquard_common::types::string::{AtUri, Cid, UriValue};
20use jacquard_common::types::uri::{RecordUri, UriError};
21use jacquard_common::xrpc::XrpcResp;
22use jacquard_derive::{IntoStatic, lexicon};
23use jacquard_lexicon::lexicon::LexiconDoc;
24use jacquard_lexicon::schema::LexiconSchema;
25
26#[allow(unused_imports)]
27use jacquard_lexicon::validation::{ConstraintError, ValidationPath};
28use serde::{Serialize, Deserialize};
29#[lexicon]
32#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)]
33#[serde(rename_all = "camelCase", rename = "sh.tangled.actor.profile", tag = "$type")]
34pub struct Profile<'a> {
35 #[serde(skip_serializing_if = "Option::is_none")]
37 #[serde(borrow)]
38 pub avatar: Option<BlobRef<'a>>,
39 pub bluesky: bool,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 #[serde(borrow)]
44 pub description: Option<CowStr<'a>>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 #[serde(borrow)]
47 pub links: Option<Vec<UriValue<'a>>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 #[serde(borrow)]
51 pub location: Option<CowStr<'a>>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 #[serde(borrow)]
55 pub pinned_repositories: Option<Vec<AtUri<'a>>>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 #[serde(borrow)]
59 pub pronouns: Option<CowStr<'a>>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 #[serde(borrow)]
62 pub stats: Option<Vec<CowStr<'a>>>,
63}
64
65#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)]
68#[serde(rename_all = "camelCase")]
69pub struct ProfileGetRecordOutput<'a> {
70 #[serde(skip_serializing_if = "Option::is_none")]
71 #[serde(borrow)]
72 pub cid: Option<Cid<'a>>,
73 #[serde(borrow)]
74 pub uri: AtUri<'a>,
75 #[serde(borrow)]
76 pub value: Profile<'a>,
77}
78
79impl<'a> Profile<'a> {
80 pub fn uri(
81 uri: impl Into<CowStr<'a>>,
82 ) -> Result<RecordUri<'a, ProfileRecord>, UriError> {
83 RecordUri::try_from_uri(AtUri::new_cow(uri.into())?)
84 }
85}
86
87#[derive(Debug, Serialize, Deserialize)]
90pub struct ProfileRecord;
91impl XrpcResp for ProfileRecord {
92 const NSID: &'static str = "sh.tangled.actor.profile";
93 const ENCODING: &'static str = "application/json";
94 type Output<'de> = ProfileGetRecordOutput<'de>;
95 type Err<'de> = RecordError<'de>;
96}
97
98impl From<ProfileGetRecordOutput<'_>> for Profile<'_> {
99 fn from(output: ProfileGetRecordOutput<'_>) -> Self {
100 use jacquard_common::IntoStatic;
101 output.value.into_static()
102 }
103}
104
105impl Collection for Profile<'_> {
106 const NSID: &'static str = "sh.tangled.actor.profile";
107 type Record = ProfileRecord;
108}
109
110impl Collection for ProfileRecord {
111 const NSID: &'static str = "sh.tangled.actor.profile";
112 type Record = ProfileRecord;
113}
114
115impl<'a> LexiconSchema for Profile<'a> {
116 fn nsid() -> &'static str {
117 "sh.tangled.actor.profile"
118 }
119 fn def_name() -> &'static str {
120 "main"
121 }
122 fn lexicon_doc() -> LexiconDoc<'static> {
123 lexicon_doc_sh_tangled_actor_profile()
124 }
125 fn validate(&self) -> Result<(), ConstraintError> {
126 if let Some(ref value) = self.avatar {
127 {
128 let size = value.blob().size;
129 if size > 1000000usize {
130 return Err(ConstraintError::BlobTooLarge {
131 path: ValidationPath::from_field("avatar"),
132 max: 1000000usize,
133 actual: size,
134 });
135 }
136 }
137 }
138 if let Some(ref value) = self.avatar {
139 {
140 let mime = value.blob().mime_type.as_str();
141 let accepted: &[&str] = &["image/png", "image/jpeg"];
142 let matched = accepted
143 .iter()
144 .any(|pattern| {
145 if *pattern == "*/*" {
146 true
147 } else if pattern.ends_with("/*") {
148 let prefix = &pattern[..pattern.len() - 2];
149 mime.starts_with(prefix)
150 && mime.as_bytes().get(prefix.len()) == Some(&b'/')
151 } else {
152 mime == *pattern
153 }
154 });
155 if !matched {
156 return Err(ConstraintError::BlobMimeTypeNotAccepted {
157 path: ValidationPath::from_field("avatar"),
158 accepted: vec![
159 "image/png".to_string(), "image/jpeg".to_string()
160 ],
161 actual: mime.to_string(),
162 });
163 }
164 }
165 }
166 if let Some(ref value) = self.description {
167 #[allow(unused_comparisons)]
168 if <str>::len(value.as_ref()) > 2560usize {
169 return Err(ConstraintError::MaxLength {
170 path: ValidationPath::from_field("description"),
171 max: 2560usize,
172 actual: <str>::len(value.as_ref()),
173 });
174 }
175 }
176 if let Some(ref value) = self.description {
177 {
178 let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count();
179 if count > 256usize {
180 return Err(ConstraintError::MaxGraphemes {
181 path: ValidationPath::from_field("description"),
182 max: 256usize,
183 actual: count,
184 });
185 }
186 }
187 }
188 if let Some(ref value) = self.links {
189 #[allow(unused_comparisons)]
190 if value.len() > 5usize {
191 return Err(ConstraintError::MaxLength {
192 path: ValidationPath::from_field("links"),
193 max: 5usize,
194 actual: value.len(),
195 });
196 }
197 }
198 if let Some(ref value) = self.links {
199 #[allow(unused_comparisons)]
200 if value.len() < 0usize {
201 return Err(ConstraintError::MinLength {
202 path: ValidationPath::from_field("links"),
203 min: 0usize,
204 actual: value.len(),
205 });
206 }
207 }
208 if let Some(ref value) = self.location {
209 #[allow(unused_comparisons)]
210 if <str>::len(value.as_ref()) > 400usize {
211 return Err(ConstraintError::MaxLength {
212 path: ValidationPath::from_field("location"),
213 max: 400usize,
214 actual: <str>::len(value.as_ref()),
215 });
216 }
217 }
218 if let Some(ref value) = self.location {
219 {
220 let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count();
221 if count > 40usize {
222 return Err(ConstraintError::MaxGraphemes {
223 path: ValidationPath::from_field("location"),
224 max: 40usize,
225 actual: count,
226 });
227 }
228 }
229 }
230 if let Some(ref value) = self.pinned_repositories {
231 #[allow(unused_comparisons)]
232 if value.len() > 6usize {
233 return Err(ConstraintError::MaxLength {
234 path: ValidationPath::from_field("pinned_repositories"),
235 max: 6usize,
236 actual: value.len(),
237 });
238 }
239 }
240 if let Some(ref value) = self.pinned_repositories {
241 #[allow(unused_comparisons)]
242 if value.len() < 0usize {
243 return Err(ConstraintError::MinLength {
244 path: ValidationPath::from_field("pinned_repositories"),
245 min: 0usize,
246 actual: value.len(),
247 });
248 }
249 }
250 if let Some(ref value) = self.pronouns {
251 #[allow(unused_comparisons)]
252 if <str>::len(value.as_ref()) > 40usize {
253 return Err(ConstraintError::MaxLength {
254 path: ValidationPath::from_field("pronouns"),
255 max: 40usize,
256 actual: <str>::len(value.as_ref()),
257 });
258 }
259 }
260 if let Some(ref value) = self.stats {
261 #[allow(unused_comparisons)]
262 if value.len() > 2usize {
263 return Err(ConstraintError::MaxLength {
264 path: ValidationPath::from_field("stats"),
265 max: 2usize,
266 actual: value.len(),
267 });
268 }
269 }
270 if let Some(ref value) = self.stats {
271 #[allow(unused_comparisons)]
272 if value.len() < 0usize {
273 return Err(ConstraintError::MinLength {
274 path: ValidationPath::from_field("stats"),
275 min: 0usize,
276 actual: value.len(),
277 });
278 }
279 }
280 Ok(())
281 }
282}
283
284pub mod profile_state {
285
286 pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
287 #[allow(unused)]
288 use ::core::marker::PhantomData;
289 mod sealed {
290 pub trait Sealed {}
291 }
292 pub trait State: sealed::Sealed {
294 type Bluesky;
295 }
296 pub struct Empty(());
298 impl sealed::Sealed for Empty {}
299 impl State for Empty {
300 type Bluesky = Unset;
301 }
302 pub struct SetBluesky<S: State = Empty>(PhantomData<fn() -> S>);
304 impl<S: State> sealed::Sealed for SetBluesky<S> {}
305 impl<S: State> State for SetBluesky<S> {
306 type Bluesky = Set<members::bluesky>;
307 }
308 #[allow(non_camel_case_types)]
310 pub mod members {
311 pub struct bluesky(());
313 }
314}
315
316pub struct ProfileBuilder<'a, S: profile_state::State> {
318 _state: PhantomData<fn() -> S>,
319 _fields: (
320 Option<BlobRef<'a>>,
321 Option<bool>,
322 Option<CowStr<'a>>,
323 Option<Vec<UriValue<'a>>>,
324 Option<CowStr<'a>>,
325 Option<Vec<AtUri<'a>>>,
326 Option<CowStr<'a>>,
327 Option<Vec<CowStr<'a>>>,
328 ),
329 _lifetime: PhantomData<&'a ()>,
330}
331
332impl<'a> Profile<'a> {
333 pub fn new() -> ProfileBuilder<'a, profile_state::Empty> {
335 ProfileBuilder::new()
336 }
337}
338
339impl<'a> ProfileBuilder<'a, profile_state::Empty> {
340 pub fn new() -> Self {
342 ProfileBuilder {
343 _state: PhantomData,
344 _fields: (None, None, None, None, None, None, None, None),
345 _lifetime: PhantomData,
346 }
347 }
348}
349
350impl<'a, S: profile_state::State> ProfileBuilder<'a, S> {
351 pub fn avatar(mut self, value: impl Into<Option<BlobRef<'a>>>) -> Self {
353 self._fields.0 = value.into();
354 self
355 }
356 pub fn maybe_avatar(mut self, value: Option<BlobRef<'a>>) -> Self {
358 self._fields.0 = value;
359 self
360 }
361}
362
363impl<'a, S> ProfileBuilder<'a, S>
364where
365 S: profile_state::State,
366 S::Bluesky: profile_state::IsUnset,
367{
368 pub fn bluesky(
370 mut self,
371 value: impl Into<bool>,
372 ) -> ProfileBuilder<'a, profile_state::SetBluesky<S>> {
373 self._fields.1 = Option::Some(value.into());
374 ProfileBuilder {
375 _state: PhantomData,
376 _fields: self._fields,
377 _lifetime: PhantomData,
378 }
379 }
380}
381
382impl<'a, S: profile_state::State> ProfileBuilder<'a, S> {
383 pub fn description(mut self, value: impl Into<Option<CowStr<'a>>>) -> Self {
385 self._fields.2 = value.into();
386 self
387 }
388 pub fn maybe_description(mut self, value: Option<CowStr<'a>>) -> Self {
390 self._fields.2 = value;
391 self
392 }
393}
394
395impl<'a, S: profile_state::State> ProfileBuilder<'a, S> {
396 pub fn links(mut self, value: impl Into<Option<Vec<UriValue<'a>>>>) -> Self {
398 self._fields.3 = value.into();
399 self
400 }
401 pub fn maybe_links(mut self, value: Option<Vec<UriValue<'a>>>) -> Self {
403 self._fields.3 = value;
404 self
405 }
406}
407
408impl<'a, S: profile_state::State> ProfileBuilder<'a, S> {
409 pub fn location(mut self, value: impl Into<Option<CowStr<'a>>>) -> Self {
411 self._fields.4 = value.into();
412 self
413 }
414 pub fn maybe_location(mut self, value: Option<CowStr<'a>>) -> Self {
416 self._fields.4 = value;
417 self
418 }
419}
420
421impl<'a, S: profile_state::State> ProfileBuilder<'a, S> {
422 pub fn pinned_repositories(
424 mut self,
425 value: impl Into<Option<Vec<AtUri<'a>>>>,
426 ) -> Self {
427 self._fields.5 = value.into();
428 self
429 }
430 pub fn maybe_pinned_repositories(mut self, value: Option<Vec<AtUri<'a>>>) -> Self {
432 self._fields.5 = value;
433 self
434 }
435}
436
437impl<'a, S: profile_state::State> ProfileBuilder<'a, S> {
438 pub fn pronouns(mut self, value: impl Into<Option<CowStr<'a>>>) -> Self {
440 self._fields.6 = value.into();
441 self
442 }
443 pub fn maybe_pronouns(mut self, value: Option<CowStr<'a>>) -> Self {
445 self._fields.6 = value;
446 self
447 }
448}
449
450impl<'a, S: profile_state::State> ProfileBuilder<'a, S> {
451 pub fn stats(mut self, value: impl Into<Option<Vec<CowStr<'a>>>>) -> Self {
453 self._fields.7 = value.into();
454 self
455 }
456 pub fn maybe_stats(mut self, value: Option<Vec<CowStr<'a>>>) -> Self {
458 self._fields.7 = value;
459 self
460 }
461}
462
463impl<'a, S> ProfileBuilder<'a, S>
464where
465 S: profile_state::State,
466 S::Bluesky: profile_state::IsSet,
467{
468 pub fn build(self) -> Profile<'a> {
470 Profile {
471 avatar: self._fields.0,
472 bluesky: self._fields.1.unwrap(),
473 description: self._fields.2,
474 links: self._fields.3,
475 location: self._fields.4,
476 pinned_repositories: self._fields.5,
477 pronouns: self._fields.6,
478 stats: self._fields.7,
479 extra_data: Default::default(),
480 }
481 }
482 pub fn build_with_data(
484 self,
485 extra_data: BTreeMap<
486 jacquard_common::deps::smol_str::SmolStr,
487 jacquard_common::types::value::Data<'a>,
488 >,
489 ) -> Profile<'a> {
490 Profile {
491 avatar: self._fields.0,
492 bluesky: self._fields.1.unwrap(),
493 description: self._fields.2,
494 links: self._fields.3,
495 location: self._fields.4,
496 pinned_repositories: self._fields.5,
497 pronouns: self._fields.6,
498 stats: self._fields.7,
499 extra_data: Some(extra_data),
500 }
501 }
502}
503
504fn lexicon_doc_sh_tangled_actor_profile() -> LexiconDoc<'static> {
505 #[allow(unused_imports)]
506 use jacquard_common::{CowStr, deps::smol_str::SmolStr, types::blob::MimeType};
507 use jacquard_lexicon::lexicon::*;
508 use alloc::collections::BTreeMap;
509 LexiconDoc {
510 lexicon: Lexicon::Lexicon1,
511 id: CowStr::new_static("sh.tangled.actor.profile"),
512 defs: {
513 let mut map = BTreeMap::new();
514 map.insert(
515 SmolStr::new_static("main"),
516 LexUserType::Record(LexRecord {
517 description: Some(
518 CowStr::new_static("A declaration of a Tangled account profile."),
519 ),
520 key: Some(CowStr::new_static("literal:self")),
521 record: LexRecordRecord::Object(LexObject {
522 required: Some(vec![SmolStr::new_static("bluesky")]),
523 properties: {
524 #[allow(unused_mut)]
525 let mut map = BTreeMap::new();
526 map.insert(
527 SmolStr::new_static("avatar"),
528 LexObjectProperty::Blob(LexBlob { ..Default::default() }),
529 );
530 map.insert(
531 SmolStr::new_static("bluesky"),
532 LexObjectProperty::Boolean(LexBoolean {
533 ..Default::default()
534 }),
535 );
536 map.insert(
537 SmolStr::new_static("description"),
538 LexObjectProperty::String(LexString {
539 description: Some(
540 CowStr::new_static("Free-form profile description text."),
541 ),
542 max_length: Some(2560usize),
543 max_graphemes: Some(256usize),
544 ..Default::default()
545 }),
546 );
547 map.insert(
548 SmolStr::new_static("links"),
549 LexObjectProperty::Array(LexArray {
550 items: LexArrayItem::String(LexString {
551 description: Some(
552 CowStr::new_static(
553 "Any URI, intended for social profiles or websites, can be used to link DIDs/AT-URIs too.",
554 ),
555 ),
556 format: Some(LexStringFormat::Uri),
557 ..Default::default()
558 }),
559 min_length: Some(0usize),
560 max_length: Some(5usize),
561 ..Default::default()
562 }),
563 );
564 map.insert(
565 SmolStr::new_static("location"),
566 LexObjectProperty::String(LexString {
567 description: Some(
568 CowStr::new_static("Free-form location text."),
569 ),
570 max_length: Some(400usize),
571 max_graphemes: Some(40usize),
572 ..Default::default()
573 }),
574 );
575 map.insert(
576 SmolStr::new_static("pinnedRepositories"),
577 LexObjectProperty::Array(LexArray {
578 description: Some(
579 CowStr::new_static(
580 "Any ATURI, it is up to appviews to validate these fields.",
581 ),
582 ),
583 items: LexArrayItem::String(LexString {
584 format: Some(LexStringFormat::AtUri),
585 ..Default::default()
586 }),
587 min_length: Some(0usize),
588 max_length: Some(6usize),
589 ..Default::default()
590 }),
591 );
592 map.insert(
593 SmolStr::new_static("pronouns"),
594 LexObjectProperty::String(LexString {
595 description: Some(
596 CowStr::new_static("Preferred gender pronouns."),
597 ),
598 max_length: Some(40usize),
599 ..Default::default()
600 }),
601 );
602 map.insert(
603 SmolStr::new_static("stats"),
604 LexObjectProperty::Array(LexArray {
605 items: LexArrayItem::String(LexString {
606 description: Some(CowStr::new_static("Vanity stats.")),
607 ..Default::default()
608 }),
609 min_length: Some(0usize),
610 max_length: Some(2usize),
611 ..Default::default()
612 }),
613 );
614 map
615 },
616 ..Default::default()
617 }),
618 ..Default::default()
619 }),
620 );
621 map
622 },
623 ..Default::default()
624 }
625}