A better Rust ATProto crate

lexicon corpus structure and test fixtures.

Orual c047f035 2760ad16

+15 -4
codegen_plan.md
···
**Tasks**:
1. Create `LexiconCorpus` struct
-
- `HashMap<SmolStr, LexiconDoc<'static>>` - NSID → doc
- Methods: `load_from_dir()`, `get()`, `resolve_ref()`
2. Load all `.json` files from lexicon directory
3. Parse into `LexiconDoc` and insert into registry
···
// ... fields
}
-
impl Collection for Post<'_> {
const NSID: &'static str = "app.bsky.feed.post";
-
type Record = Post<'static>;
}
```
···
/// The NSID for this XRPC method
const NSID: &'static str;
-
/// HTTP method (GET for queries, POST for procedures)
const METHOD: XrpcMethod;
/// Input encoding (MIME type, e.g., "application/json")
···
/// Response output type
type Output: Deserialize<'x>;
}
pub enum XrpcMethod {
···
}
```
**Generated implementation:**
```rust
pub struct GetAuthorFeedParams<'a> {
···
type Params = Self;
type Output = GetAuthorFeedOutput<'static>;
}
```
···
- Allows monomorphization (static dispatch) for performance
- Also supports `dyn XrpcRequest` for dynamic dispatch if needed
- Client code can be generic over `impl XrpcRequest`
### Subscriptions
WebSocket streams - defer for now. Will need separate trait with message types.
···
**Tasks**:
1. Create `LexiconCorpus` struct
+
- `BTreeMap<SmolStr, LexiconDoc<'static>>` - NSID → doc
- Methods: `load_from_dir()`, `get()`, `resolve_ref()`
2. Load all `.json` files from lexicon directory
3. Parse into `LexiconDoc` and insert into registry
···
// ... fields
}
+
impl Collection for Post<'p> {
const NSID: &'static str = "app.bsky.feed.post";
+
type Record = Post<'p>;
}
```
···
/// The NSID for this XRPC method
const NSID: &'static str;
+
/// XRPC method (query/GET, procedure/POST)
const METHOD: XrpcMethod;
/// Input encoding (MIME type, e.g., "application/json")
···
/// Response output type
type Output: Deserialize<'x>;
+
+
type Err: Error;
}
pub enum XrpcMethod {
···
}
```
+
+
**Generated implementation:**
```rust
pub struct GetAuthorFeedParams<'a> {
···
type Params = Self;
type Output = GetAuthorFeedOutput<'static>;
+
type Err = GetAuthorFeedError;
}
```
···
- Allows monomorphization (static dispatch) for performance
- Also supports `dyn XrpcRequest` for dynamic dispatch if needed
- Client code can be generic over `impl XrpcRequest`
+
+
+
#### XRPC Errors
+
Lexicons contain information on the kind of errors they can return.
+
Trait contains an associated error type. Error enum with thiserror::Error and
+
miette:Diagnostic derives and appropriate content generated based on lexicon info.
### Subscriptions
WebSocket streams - defer for now. Will need separate trait with message types.
+34 -1
crates/jacquard-common/src/into_static.rs
···
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
···
}
impl_into_static_passthru!(
-
String, u128, u64, u32, u16, u8, i128, i64, i32, i16, i8, bool, char, usize, isize, f32, f64
);
impl<T: IntoStatic> IntoStatic for Box<T> {
···
K::Output: Eq + Hash,
{
type Output = HashMap<K::Output, V::Output, S>;
fn into_static(self) -> Self::Output {
self.into_iter()
···
use std::borrow::Cow;
+
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
···
}
impl_into_static_passthru!(
+
String,
+
u128,
+
u64,
+
u32,
+
u16,
+
u8,
+
i128,
+
i64,
+
i32,
+
i16,
+
i8,
+
bool,
+
char,
+
usize,
+
isize,
+
f32,
+
f64,
+
crate::smol_str::SmolStr
);
impl<T: IntoStatic> IntoStatic for Box<T> {
···
K::Output: Eq + Hash,
{
type Output = HashMap<K::Output, V::Output, S>;
+
+
fn into_static(self) -> Self::Output {
+
self.into_iter()
+
.map(|(k, v)| (k.into_static(), v.into_static()))
+
.collect()
+
}
+
}
+
+
impl<K, V> IntoStatic for BTreeMap<K, V>
+
where
+
K: IntoStatic + Ord,
+
V: IntoStatic,
+
K::Output: Ord,
+
{
+
type Output = BTreeMap<K::Output, V::Output>;
fn into_static(self) -> Self::Output {
self.into_iter()
+7 -11
crates/jacquard-common/src/types/value/convert.rs
···
-
use core::{any::TypeId, fmt};
-
use std::{borrow::ToOwned, boxed::Box, collections::BTreeMap, vec::Vec};
-
-
use crate::{
-
CowStr,
-
types::{
-
DataModelType,
-
cid::Cid,
-
string::AtprotoStr,
-
value::{Array, Data, Object},
-
},
};
use bytes::Bytes;
use smol_str::SmolStr;
/// Error used for converting from and into [`crate::types::value::Data`].
#[derive(Clone, Debug)]
···
+
use crate::types::{
+
DataModelType,
+
cid::Cid,
+
string::AtprotoStr,
+
value::{Array, Data, Object},
};
use bytes::Bytes;
+
use core::{any::TypeId, fmt};
use smol_str::SmolStr;
+
use std::{borrow::ToOwned, boxed::Box, collections::BTreeMap, vec::Vec};
/// Error used for converting from and into [`crate::types::value::Data`].
#[derive(Clone, Debug)]
+162
crates/jacquard-lexicon/src/corpus.rs
···
···
+
use crate::lexicon::{LexiconDoc, LexUserType};
+
use jacquard_common::{into_static::IntoStatic, smol_str::SmolStr};
+
use std::collections::BTreeMap;
+
use std::fs;
+
use std::io;
+
use std::path::Path;
+
+
/// Registry of all loaded lexicons for reference resolution
+
#[derive(Debug, Clone)]
+
pub struct LexiconCorpus {
+
/// Map from NSID to lexicon document
+
docs: BTreeMap<SmolStr, LexiconDoc<'static>>,
+
}
+
+
impl LexiconCorpus {
+
/// Create an empty corpus
+
pub fn new() -> Self {
+
Self {
+
docs: BTreeMap::new(),
+
}
+
}
+
+
/// Load all lexicons from a directory
+
pub fn load_from_dir(path: impl AsRef<Path>) -> io::Result<Self> {
+
let mut corpus = Self::new();
+
+
let schemas = crate::fs::find_schemas(path.as_ref())?;
+
for schema_path in schemas {
+
let content = fs::read_to_string(schema_path.as_ref())?;
+
let doc: LexiconDoc = serde_json::from_str(&content).map_err(|e| {
+
io::Error::new(
+
io::ErrorKind::InvalidData,
+
format!("Failed to parse {}: {}", schema_path.as_ref().display(), e),
+
)
+
})?;
+
+
let nsid = SmolStr::from(doc.id.to_string());
+
corpus.docs.insert(nsid, doc.into_static());
+
}
+
+
Ok(corpus)
+
}
+
+
/// Get a lexicon document by NSID
+
pub fn get(&self, nsid: &str) -> Option<&LexiconDoc<'static>> {
+
self.docs.get(nsid)
+
}
+
+
/// Resolve a reference, handling fragments
+
///
+
/// Examples:
+
/// - `app.bsky.feed.post` → main def from that lexicon
+
/// - `app.bsky.feed.post#replyRef` → replyRef def from that lexicon
+
pub fn resolve_ref(&self, ref_str: &str) -> Option<(&LexiconDoc<'static>, &LexUserType<'static>)> {
+
let (nsid, def_name) = if let Some((nsid, fragment)) = ref_str.split_once('#') {
+
(nsid, fragment)
+
} else {
+
(ref_str, "main")
+
};
+
+
let doc = self.get(nsid)?;
+
let def = doc.defs.get(def_name)?;
+
Some((doc, def))
+
}
+
+
/// Check if a reference exists
+
pub fn ref_exists(&self, ref_str: &str) -> bool {
+
self.resolve_ref(ref_str).is_some()
+
}
+
+
/// Iterate over all documents
+
pub fn iter(&self) -> impl Iterator<Item = (&SmolStr, &LexiconDoc<'static>)> {
+
self.docs.iter()
+
}
+
+
/// Number of loaded lexicons
+
pub fn len(&self) -> usize {
+
self.docs.len()
+
}
+
+
/// Check if corpus is empty
+
pub fn is_empty(&self) -> bool {
+
self.docs.is_empty()
+
}
+
}
+
+
impl Default for LexiconCorpus {
+
fn default() -> Self {
+
Self::new()
+
}
+
}
+
+
#[cfg(test)]
+
mod tests {
+
use super::*;
+
use crate::lexicon::LexUserType;
+
+
#[test]
+
fn test_empty_corpus() {
+
let corpus = LexiconCorpus::new();
+
assert!(corpus.is_empty());
+
assert_eq!(corpus.len(), 0);
+
}
+
+
#[test]
+
fn test_load_real_lexicons() {
+
let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons")
+
.expect("failed to load lexicons");
+
+
assert!(!corpus.is_empty());
+
assert_eq!(corpus.len(), 10);
+
+
// Check that we loaded the expected lexicons
+
assert!(corpus.get("app.bsky.feed.post").is_some());
+
assert!(corpus.get("app.bsky.feed.getAuthorFeed").is_some());
+
assert!(corpus.get("app.bsky.richtext.facet").is_some());
+
assert!(corpus.get("app.bsky.embed.images").is_some());
+
assert!(corpus.get("com.atproto.repo.strongRef").is_some());
+
assert!(corpus.get("com.atproto.label.defs").is_some());
+
}
+
+
#[test]
+
fn test_resolve_ref_without_fragment() {
+
let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons")
+
.expect("failed to load lexicons");
+
+
// Without fragment should resolve to main def
+
let (doc, def) = corpus
+
.resolve_ref("app.bsky.feed.post")
+
.expect("should resolve");
+
assert_eq!(doc.id.as_ref(), "app.bsky.feed.post");
+
assert!(matches!(def, LexUserType::Record(_)));
+
}
+
+
#[test]
+
fn test_resolve_ref_with_fragment() {
+
let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons")
+
.expect("failed to load lexicons");
+
+
// With fragment should resolve to specific def
+
let (doc, def) = corpus
+
.resolve_ref("app.bsky.richtext.facet#mention")
+
.expect("should resolve");
+
assert_eq!(doc.id.as_ref(), "app.bsky.richtext.facet");
+
assert!(matches!(def, LexUserType::Object(_)));
+
}
+
+
#[test]
+
fn test_ref_exists() {
+
let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons")
+
.expect("failed to load lexicons");
+
+
// Existing refs
+
assert!(corpus.ref_exists("app.bsky.feed.post"));
+
assert!(corpus.ref_exists("app.bsky.feed.post#main"));
+
assert!(corpus.ref_exists("app.bsky.richtext.facet#mention"));
+
+
// Non-existing refs
+
assert!(!corpus.ref_exists("com.example.fake"));
+
assert!(!corpus.ref_exists("app.bsky.feed.post#nonexistent"));
+
}
+
}
+433 -1
crates/jacquard-lexicon/src/lexicon.rs
···
// https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lexicon.rs
// https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lib.rs
-
use jacquard_common::{CowStr, smol_str::SmolStr, types::blob::MimeType};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_with::skip_serializing_none;
···
CidLink(LexCidLink<'s>),
// lexUnknown
Unknown(LexUnknown<'s>),
}
#[cfg(test)]
···
// https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lexicon.rs
// https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lib.rs
+
use jacquard_common::{into_static::IntoStatic, smol_str::SmolStr, types::blob::MimeType, CowStr};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_with::skip_serializing_none;
···
CidLink(LexCidLink<'s>),
// lexUnknown
Unknown(LexUnknown<'s>),
+
}
+
+
// IntoStatic implementations for all lexicon types
+
// These enable converting borrowed lexicon docs to owned 'static versions
+
+
macro_rules! impl_into_static_for_lex_struct {
+
($($ty:ident),+ $(,)?) => {
+
$(
+
impl IntoStatic for $ty<'_> {
+
type Output = $ty<'static>;
+
+
fn into_static(self) -> Self::Output {
+
let Self {
+
$(description,)?
+
..$fields
+
} = self;
+
Self::Output {
+
$(description: description.into_static(),)?
+
..$fields.into_static()
+
}
+
}
+
}
+
)+
+
};
+
}
+
+
// Simpler approach: just clone and convert each field
+
impl IntoStatic for Lexicon {
+
type Output = Lexicon;
+
fn into_static(self) -> Self::Output {
+
self
+
}
+
}
+
+
impl IntoStatic for LexStringFormat {
+
type Output = LexStringFormat;
+
fn into_static(self) -> Self::Output {
+
self
+
}
+
}
+
+
impl IntoStatic for LexiconDoc<'_> {
+
type Output = LexiconDoc<'static>;
+
fn into_static(self) -> Self::Output {
+
LexiconDoc {
+
lexicon: self.lexicon,
+
id: self.id.into_static(),
+
revision: self.revision,
+
description: self.description.into_static(),
+
defs: self.defs.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexBoolean<'_> {
+
type Output = LexBoolean<'static>;
+
fn into_static(self) -> Self::Output {
+
LexBoolean {
+
description: self.description.into_static(),
+
default: self.default,
+
r#const: self.r#const,
+
}
+
}
+
}
+
+
impl IntoStatic for LexInteger<'_> {
+
type Output = LexInteger<'static>;
+
fn into_static(self) -> Self::Output {
+
LexInteger {
+
description: self.description.into_static(),
+
default: self.default,
+
minimum: self.minimum,
+
maximum: self.maximum,
+
r#enum: self.r#enum,
+
r#const: self.r#const,
+
}
+
}
+
}
+
+
impl IntoStatic for LexString<'_> {
+
type Output = LexString<'static>;
+
fn into_static(self) -> Self::Output {
+
LexString {
+
description: self.description.into_static(),
+
format: self.format,
+
default: self.default.into_static(),
+
min_length: self.min_length,
+
max_length: self.max_length,
+
min_graphemes: self.min_graphemes,
+
max_graphemes: self.max_graphemes,
+
r#enum: self.r#enum.into_static(),
+
r#const: self.r#const.into_static(),
+
known_values: self.known_values.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexUnknown<'_> {
+
type Output = LexUnknown<'static>;
+
fn into_static(self) -> Self::Output {
+
LexUnknown {
+
description: self.description.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexBytes<'_> {
+
type Output = LexBytes<'static>;
+
fn into_static(self) -> Self::Output {
+
LexBytes {
+
description: self.description.into_static(),
+
max_length: self.max_length,
+
min_length: self.min_length,
+
}
+
}
+
}
+
+
impl IntoStatic for LexCidLink<'_> {
+
type Output = LexCidLink<'static>;
+
fn into_static(self) -> Self::Output {
+
LexCidLink {
+
description: self.description.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexRef<'_> {
+
type Output = LexRef<'static>;
+
fn into_static(self) -> Self::Output {
+
LexRef {
+
description: self.description.into_static(),
+
r#ref: self.r#ref.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexRefUnion<'_> {
+
type Output = LexRefUnion<'static>;
+
fn into_static(self) -> Self::Output {
+
LexRefUnion {
+
description: self.description.into_static(),
+
refs: self.refs.into_static(),
+
closed: self.closed,
+
}
+
}
+
}
+
+
impl IntoStatic for LexBlob<'_> {
+
type Output = LexBlob<'static>;
+
fn into_static(self) -> Self::Output {
+
LexBlob {
+
description: self.description.into_static(),
+
accept: self.accept.into_static(),
+
max_size: self.max_size,
+
}
+
}
+
}
+
+
impl IntoStatic for LexArrayItem<'_> {
+
type Output = LexArrayItem<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Boolean(x) => LexArrayItem::Boolean(x.into_static()),
+
Self::Integer(x) => LexArrayItem::Integer(x.into_static()),
+
Self::String(x) => LexArrayItem::String(x.into_static()),
+
Self::Unknown(x) => LexArrayItem::Unknown(x.into_static()),
+
Self::Bytes(x) => LexArrayItem::Bytes(x.into_static()),
+
Self::CidLink(x) => LexArrayItem::CidLink(x.into_static()),
+
Self::Blob(x) => LexArrayItem::Blob(x.into_static()),
+
Self::Ref(x) => LexArrayItem::Ref(x.into_static()),
+
Self::Union(x) => LexArrayItem::Union(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexArray<'_> {
+
type Output = LexArray<'static>;
+
fn into_static(self) -> Self::Output {
+
LexArray {
+
description: self.description.into_static(),
+
items: self.items.into_static(),
+
min_length: self.min_length,
+
max_length: self.max_length,
+
}
+
}
+
}
+
+
impl IntoStatic for LexPrimitiveArrayItem<'_> {
+
type Output = LexPrimitiveArrayItem<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Boolean(x) => LexPrimitiveArrayItem::Boolean(x.into_static()),
+
Self::Integer(x) => LexPrimitiveArrayItem::Integer(x.into_static()),
+
Self::String(x) => LexPrimitiveArrayItem::String(x.into_static()),
+
Self::Unknown(x) => LexPrimitiveArrayItem::Unknown(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexPrimitiveArray<'_> {
+
type Output = LexPrimitiveArray<'static>;
+
fn into_static(self) -> Self::Output {
+
LexPrimitiveArray {
+
description: self.description.into_static(),
+
items: self.items.into_static(),
+
min_length: self.min_length,
+
max_length: self.max_length,
+
}
+
}
+
}
+
+
impl IntoStatic for LexToken<'_> {
+
type Output = LexToken<'static>;
+
fn into_static(self) -> Self::Output {
+
LexToken {
+
description: self.description.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexObjectProperty<'_> {
+
type Output = LexObjectProperty<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Ref(x) => LexObjectProperty::Ref(x.into_static()),
+
Self::Union(x) => LexObjectProperty::Union(x.into_static()),
+
Self::Bytes(x) => LexObjectProperty::Bytes(x.into_static()),
+
Self::CidLink(x) => LexObjectProperty::CidLink(x.into_static()),
+
Self::Array(x) => LexObjectProperty::Array(x.into_static()),
+
Self::Blob(x) => LexObjectProperty::Blob(x.into_static()),
+
Self::Boolean(x) => LexObjectProperty::Boolean(x.into_static()),
+
Self::Integer(x) => LexObjectProperty::Integer(x.into_static()),
+
Self::String(x) => LexObjectProperty::String(x.into_static()),
+
Self::Unknown(x) => LexObjectProperty::Unknown(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexObject<'_> {
+
type Output = LexObject<'static>;
+
fn into_static(self) -> Self::Output {
+
LexObject {
+
description: self.description.into_static(),
+
required: self.required,
+
nullable: self.nullable,
+
properties: self.properties.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcParametersProperty<'_> {
+
type Output = LexXrpcParametersProperty<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Boolean(x) => LexXrpcParametersProperty::Boolean(x.into_static()),
+
Self::Integer(x) => LexXrpcParametersProperty::Integer(x.into_static()),
+
Self::String(x) => LexXrpcParametersProperty::String(x.into_static()),
+
Self::Unknown(x) => LexXrpcParametersProperty::Unknown(x.into_static()),
+
Self::Array(x) => LexXrpcParametersProperty::Array(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcParameters<'_> {
+
type Output = LexXrpcParameters<'static>;
+
fn into_static(self) -> Self::Output {
+
LexXrpcParameters {
+
description: self.description.into_static(),
+
required: self.required,
+
properties: self.properties.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcBodySchema<'_> {
+
type Output = LexXrpcBodySchema<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Ref(x) => LexXrpcBodySchema::Ref(x.into_static()),
+
Self::Union(x) => LexXrpcBodySchema::Union(x.into_static()),
+
Self::Object(x) => LexXrpcBodySchema::Object(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcBody<'_> {
+
type Output = LexXrpcBody<'static>;
+
fn into_static(self) -> Self::Output {
+
LexXrpcBody {
+
description: self.description.into_static(),
+
encoding: self.encoding.into_static(),
+
schema: self.schema.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcSubscriptionMessageSchema<'_> {
+
type Output = LexXrpcSubscriptionMessageSchema<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Ref(x) => LexXrpcSubscriptionMessageSchema::Ref(x.into_static()),
+
Self::Union(x) => LexXrpcSubscriptionMessageSchema::Union(x.into_static()),
+
Self::Object(x) => LexXrpcSubscriptionMessageSchema::Object(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcSubscriptionMessage<'_> {
+
type Output = LexXrpcSubscriptionMessage<'static>;
+
fn into_static(self) -> Self::Output {
+
LexXrpcSubscriptionMessage {
+
description: self.description.into_static(),
+
schema: self.schema.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcError<'_> {
+
type Output = LexXrpcError<'static>;
+
fn into_static(self) -> Self::Output {
+
LexXrpcError {
+
description: self.description.into_static(),
+
name: self.name.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcQueryParameter<'_> {
+
type Output = LexXrpcQueryParameter<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Params(x) => LexXrpcQueryParameter::Params(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcQuery<'_> {
+
type Output = LexXrpcQuery<'static>;
+
fn into_static(self) -> Self::Output {
+
LexXrpcQuery {
+
description: self.description.into_static(),
+
parameters: self.parameters.into_static(),
+
output: self.output.into_static(),
+
errors: self.errors.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcProcedureParameter<'_> {
+
type Output = LexXrpcProcedureParameter<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Params(x) => LexXrpcProcedureParameter::Params(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcProcedure<'_> {
+
type Output = LexXrpcProcedure<'static>;
+
fn into_static(self) -> Self::Output {
+
LexXrpcProcedure {
+
description: self.description.into_static(),
+
parameters: self.parameters.into_static(),
+
input: self.input.into_static(),
+
output: self.output.into_static(),
+
errors: self.errors.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcSubscriptionParameter<'_> {
+
type Output = LexXrpcSubscriptionParameter<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Params(x) => LexXrpcSubscriptionParameter::Params(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexXrpcSubscription<'_> {
+
type Output = LexXrpcSubscription<'static>;
+
fn into_static(self) -> Self::Output {
+
LexXrpcSubscription {
+
description: self.description.into_static(),
+
parameters: self.parameters.into_static(),
+
message: self.message.into_static(),
+
infos: self.infos.into_static(),
+
errors: self.errors.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexRecordRecord<'_> {
+
type Output = LexRecordRecord<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Object(x) => LexRecordRecord::Object(x.into_static()),
+
}
+
}
+
}
+
+
impl IntoStatic for LexRecord<'_> {
+
type Output = LexRecord<'static>;
+
fn into_static(self) -> Self::Output {
+
LexRecord {
+
description: self.description.into_static(),
+
key: self.key.into_static(),
+
record: self.record.into_static(),
+
}
+
}
+
}
+
+
impl IntoStatic for LexUserType<'_> {
+
type Output = LexUserType<'static>;
+
fn into_static(self) -> Self::Output {
+
match self {
+
Self::Record(x) => LexUserType::Record(x.into_static()),
+
Self::XrpcQuery(x) => LexUserType::XrpcQuery(x.into_static()),
+
Self::XrpcProcedure(x) => LexUserType::XrpcProcedure(x.into_static()),
+
Self::XrpcSubscription(x) => LexUserType::XrpcSubscription(x.into_static()),
+
Self::Blob(x) => LexUserType::Blob(x.into_static()),
+
Self::Array(x) => LexUserType::Array(x.into_static()),
+
Self::Token(x) => LexUserType::Token(x.into_static()),
+
Self::Object(x) => LexUserType::Object(x.into_static()),
+
Self::Boolean(x) => LexUserType::Boolean(x.into_static()),
+
Self::Integer(x) => LexUserType::Integer(x.into_static()),
+
Self::String(x) => LexUserType::String(x.into_static()),
+
Self::Bytes(x) => LexUserType::Bytes(x.into_static()),
+
Self::CidLink(x) => LexUserType::CidLink(x.into_static()),
+
Self::Unknown(x) => LexUserType::Unknown(x.into_static()),
+
}
+
}
}
#[cfg(test)]
+1
crates/jacquard-lexicon/src/lib.rs
···
pub mod fs;
pub mod lexicon;
pub mod output;
···
+
pub mod corpus;
pub mod fs;
pub mod lexicon;
pub mod output;
+156
crates/jacquard-lexicon/tests/fixtures/lexicons/defs.json
···
···
+
{
+
"lexicon": 1,
+
"id": "com.atproto.label.defs",
+
"defs": {
+
"label": {
+
"type": "object",
+
"description": "Metadata tag on an atproto resource (eg, repo or record).",
+
"required": ["src", "uri", "val", "cts"],
+
"properties": {
+
"ver": {
+
"type": "integer",
+
"description": "The AT Protocol version of the label object."
+
},
+
"src": {
+
"type": "string",
+
"format": "did",
+
"description": "DID of the actor who created this label."
+
},
+
"uri": {
+
"type": "string",
+
"format": "uri",
+
"description": "AT URI of the record, repository (account), or other resource that this label applies to."
+
},
+
"cid": {
+
"type": "string",
+
"format": "cid",
+
"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
+
},
+
"val": {
+
"type": "string",
+
"maxLength": 128,
+
"description": "The short string name of the value or type of this label."
+
},
+
"neg": {
+
"type": "boolean",
+
"description": "If true, this is a negation label, overwriting a previous label."
+
},
+
"cts": {
+
"type": "string",
+
"format": "datetime",
+
"description": "Timestamp when this label was created."
+
},
+
"exp": {
+
"type": "string",
+
"format": "datetime",
+
"description": "Timestamp at which this label expires (no longer applies)."
+
},
+
"sig": {
+
"type": "bytes",
+
"description": "Signature of dag-cbor encoded label."
+
}
+
}
+
},
+
"selfLabels": {
+
"type": "object",
+
"description": "Metadata tags on an atproto record, published by the author within the record.",
+
"required": ["values"],
+
"properties": {
+
"values": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "#selfLabel" },
+
"maxLength": 10
+
}
+
}
+
},
+
"selfLabel": {
+
"type": "object",
+
"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
+
"required": ["val"],
+
"properties": {
+
"val": {
+
"type": "string",
+
"maxLength": 128,
+
"description": "The short string name of the value or type of this label."
+
}
+
}
+
},
+
"labelValueDefinition": {
+
"type": "object",
+
"description": "Declares a label value and its expected interpretations and behaviors.",
+
"required": ["identifier", "severity", "blurs", "locales"],
+
"properties": {
+
"identifier": {
+
"type": "string",
+
"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
+
"maxLength": 100,
+
"maxGraphemes": 100
+
},
+
"severity": {
+
"type": "string",
+
"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
+
"knownValues": ["inform", "alert", "none"]
+
},
+
"blurs": {
+
"type": "string",
+
"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
+
"knownValues": ["content", "media", "none"]
+
},
+
"defaultSetting": {
+
"type": "string",
+
"description": "The default setting for this label.",
+
"knownValues": ["ignore", "warn", "hide"],
+
"default": "warn"
+
},
+
"adultOnly": {
+
"type": "boolean",
+
"description": "Does the user need to have adult content enabled in order to configure this label?"
+
},
+
"locales": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
+
}
+
}
+
},
+
"labelValueDefinitionStrings": {
+
"type": "object",
+
"description": "Strings which describe the label in the UI, localized into a specific language.",
+
"required": ["lang", "name", "description"],
+
"properties": {
+
"lang": {
+
"type": "string",
+
"description": "The code of the language these strings are written in.",
+
"format": "language"
+
},
+
"name": {
+
"type": "string",
+
"description": "A short human-readable name for the label.",
+
"maxGraphemes": 64,
+
"maxLength": 640
+
},
+
"description": {
+
"type": "string",
+
"description": "A longer description of what the label means and why it might be applied.",
+
"maxGraphemes": 10000,
+
"maxLength": 100000
+
}
+
}
+
},
+
"labelValue": {
+
"type": "string",
+
"knownValues": [
+
"!hide",
+
"!no-promote",
+
"!warn",
+
"!no-unauthenticated",
+
"dmca-violation",
+
"doxxing",
+
"porn",
+
"sexual",
+
"nudity",
+
"nsfl",
+
"gore"
+
]
+
}
+
}
+
}
+51
crates/jacquard-lexicon/tests/fixtures/lexicons/external.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.embed.external",
+
"defs": {
+
"main": {
+
"type": "object",
+
"description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).",
+
"required": ["external"],
+
"properties": {
+
"external": {
+
"type": "ref",
+
"ref": "#external"
+
}
+
}
+
},
+
"external": {
+
"type": "object",
+
"required": ["uri", "title", "description"],
+
"properties": {
+
"uri": { "type": "string", "format": "uri" },
+
"title": { "type": "string" },
+
"description": { "type": "string" },
+
"thumb": {
+
"type": "blob",
+
"accept": ["image/*"],
+
"maxSize": 1000000
+
}
+
}
+
},
+
"view": {
+
"type": "object",
+
"required": ["external"],
+
"properties": {
+
"external": {
+
"type": "ref",
+
"ref": "#viewExternal"
+
}
+
}
+
},
+
"viewExternal": {
+
"type": "object",
+
"required": ["uri", "title", "description"],
+
"properties": {
+
"uri": { "type": "string", "format": "uri" },
+
"title": { "type": "string" },
+
"description": { "type": "string" },
+
"thumb": { "type": "string", "format": "uri" }
+
}
+
}
+
}
+
}
+51
crates/jacquard-lexicon/tests/fixtures/lexicons/facet.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.richtext.facet",
+
"defs": {
+
"main": {
+
"type": "object",
+
"description": "Annotation of a sub-string within rich text.",
+
"required": ["index", "features"],
+
"properties": {
+
"index": { "type": "ref", "ref": "#byteSlice" },
+
"features": {
+
"type": "array",
+
"items": { "type": "union", "refs": ["#mention", "#link", "#tag"] }
+
}
+
}
+
},
+
"mention": {
+
"type": "object",
+
"description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.",
+
"required": ["did"],
+
"properties": {
+
"did": { "type": "string", "format": "did" }
+
}
+
},
+
"link": {
+
"type": "object",
+
"description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.",
+
"required": ["uri"],
+
"properties": {
+
"uri": { "type": "string", "format": "uri" }
+
}
+
},
+
"tag": {
+
"type": "object",
+
"description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').",
+
"required": ["tag"],
+
"properties": {
+
"tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }
+
}
+
},
+
"byteSlice": {
+
"type": "object",
+
"description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.",
+
"required": ["byteStart", "byteEnd"],
+
"properties": {
+
"byteStart": { "type": "integer", "minimum": 0 },
+
"byteEnd": { "type": "integer", "minimum": 0 }
+
}
+
}
+
}
+
}
+58
crates/jacquard-lexicon/tests/fixtures/lexicons/getAuthorFeed.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.feed.getAuthorFeed",
+
"defs": {
+
"main": {
+
"type": "query",
+
"description": "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.",
+
"parameters": {
+
"type": "params",
+
"required": ["actor"],
+
"properties": {
+
"actor": { "type": "string", "format": "at-identifier" },
+
"limit": {
+
"type": "integer",
+
"minimum": 1,
+
"maximum": 100,
+
"default": 50
+
},
+
"cursor": { "type": "string" },
+
"filter": {
+
"type": "string",
+
"description": "Combinations of post/repost types to include in response.",
+
"knownValues": [
+
"posts_with_replies",
+
"posts_no_replies",
+
"posts_with_media",
+
"posts_and_author_threads",
+
"posts_with_video"
+
],
+
"default": "posts_with_replies"
+
},
+
"includePins": {
+
"type": "boolean",
+
"default": false
+
}
+
}
+
},
+
"output": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["feed"],
+
"properties": {
+
"cursor": { "type": "string" },
+
"feed": {
+
"type": "array",
+
"items": {
+
"type": "ref",
+
"ref": "app.bsky.feed.defs#feedViewPost"
+
}
+
}
+
}
+
}
+
},
+
"errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }]
+
}
+
}
+
}
+72
crates/jacquard-lexicon/tests/fixtures/lexicons/images.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.embed.images",
+
"description": "A set of images embedded in a Bluesky record (eg, a post).",
+
"defs": {
+
"main": {
+
"type": "object",
+
"required": ["images"],
+
"properties": {
+
"images": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "#image" },
+
"maxLength": 4
+
}
+
}
+
},
+
"image": {
+
"type": "object",
+
"required": ["image", "alt"],
+
"properties": {
+
"image": {
+
"type": "blob",
+
"accept": ["image/*"],
+
"maxSize": 1000000
+
},
+
"alt": {
+
"type": "string",
+
"description": "Alt text description of the image, for accessibility."
+
},
+
"aspectRatio": {
+
"type": "ref",
+
"ref": "app.bsky.embed.defs#aspectRatio"
+
}
+
}
+
},
+
"view": {
+
"type": "object",
+
"required": ["images"],
+
"properties": {
+
"images": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "#viewImage" },
+
"maxLength": 4
+
}
+
}
+
},
+
"viewImage": {
+
"type": "object",
+
"required": ["thumb", "fullsize", "alt"],
+
"properties": {
+
"thumb": {
+
"type": "string",
+
"format": "uri",
+
"description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View."
+
},
+
"fullsize": {
+
"type": "string",
+
"format": "uri",
+
"description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View."
+
},
+
"alt": {
+
"type": "string",
+
"description": "Alt text description of the image, for accessibility."
+
},
+
"aspectRatio": {
+
"type": "ref",
+
"ref": "app.bsky.embed.defs#aspectRatio"
+
}
+
}
+
}
+
}
+
}
+96
crates/jacquard-lexicon/tests/fixtures/lexicons/post.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.feed.post",
+
"defs": {
+
"main": {
+
"type": "record",
+
"description": "Record containing a Bluesky post.",
+
"key": "tid",
+
"record": {
+
"type": "object",
+
"required": ["text", "createdAt"],
+
"properties": {
+
"text": {
+
"type": "string",
+
"maxLength": 3000,
+
"maxGraphemes": 300,
+
"description": "The primary post content. May be an empty string, if there are embeds."
+
},
+
"entities": {
+
"type": "array",
+
"description": "DEPRECATED: replaced by app.bsky.richtext.facet.",
+
"items": { "type": "ref", "ref": "#entity" }
+
},
+
"facets": {
+
"type": "array",
+
"description": "Annotations of text (mentions, URLs, hashtags, etc)",
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+
},
+
"reply": { "type": "ref", "ref": "#replyRef" },
+
"embed": {
+
"type": "union",
+
"refs": [
+
"app.bsky.embed.images",
+
"app.bsky.embed.video",
+
"app.bsky.embed.external",
+
"app.bsky.embed.record",
+
"app.bsky.embed.recordWithMedia"
+
]
+
},
+
"langs": {
+
"type": "array",
+
"description": "Indicates human language of post primary text content.",
+
"maxLength": 3,
+
"items": { "type": "string", "format": "language" }
+
},
+
"labels": {
+
"type": "union",
+
"description": "Self-label values for this post. Effectively content warnings.",
+
"refs": ["com.atproto.label.defs#selfLabels"]
+
},
+
"tags": {
+
"type": "array",
+
"description": "Additional hashtags, in addition to any included in post text and facets.",
+
"maxLength": 8,
+
"items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }
+
},
+
"createdAt": {
+
"type": "string",
+
"format": "datetime",
+
"description": "Client-declared timestamp when this post was originally created."
+
}
+
}
+
}
+
},
+
"replyRef": {
+
"type": "object",
+
"required": ["root", "parent"],
+
"properties": {
+
"root": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
+
"parent": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
+
}
+
},
+
"entity": {
+
"type": "object",
+
"description": "Deprecated: use facets instead.",
+
"required": ["index", "type", "value"],
+
"properties": {
+
"index": { "type": "ref", "ref": "#textSlice" },
+
"type": {
+
"type": "string",
+
"description": "Expected values are 'mention' and 'link'."
+
},
+
"value": { "type": "string" }
+
}
+
},
+
"textSlice": {
+
"type": "object",
+
"description": "Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.",
+
"required": ["start", "end"],
+
"properties": {
+
"start": { "type": "integer", "minimum": 0 },
+
"end": { "type": "integer", "minimum": 0 }
+
}
+
}
+
}
+
}
+96
crates/jacquard-lexicon/tests/fixtures/lexicons/record.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.embed.record",
+
"description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.",
+
"defs": {
+
"main": {
+
"type": "object",
+
"required": ["record"],
+
"properties": {
+
"record": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
+
}
+
},
+
"view": {
+
"type": "object",
+
"required": ["record"],
+
"properties": {
+
"record": {
+
"type": "union",
+
"refs": [
+
"#viewRecord",
+
"#viewNotFound",
+
"#viewBlocked",
+
"#viewDetached",
+
"app.bsky.feed.defs#generatorView",
+
"app.bsky.graph.defs#listView",
+
"app.bsky.labeler.defs#labelerView",
+
"app.bsky.graph.defs#starterPackViewBasic"
+
]
+
}
+
}
+
},
+
"viewRecord": {
+
"type": "object",
+
"required": ["uri", "cid", "author", "value", "indexedAt"],
+
"properties": {
+
"uri": { "type": "string", "format": "at-uri" },
+
"cid": { "type": "string", "format": "cid" },
+
"author": {
+
"type": "ref",
+
"ref": "app.bsky.actor.defs#profileViewBasic"
+
},
+
"value": {
+
"type": "unknown",
+
"description": "The record data itself."
+
},
+
"labels": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+
},
+
"replyCount": { "type": "integer" },
+
"repostCount": { "type": "integer" },
+
"likeCount": { "type": "integer" },
+
"quoteCount": { "type": "integer" },
+
"embeds": {
+
"type": "array",
+
"items": {
+
"type": "union",
+
"refs": [
+
"app.bsky.embed.images#view",
+
"app.bsky.embed.video#view",
+
"app.bsky.embed.external#view",
+
"app.bsky.embed.record#view",
+
"app.bsky.embed.recordWithMedia#view"
+
]
+
}
+
},
+
"indexedAt": { "type": "string", "format": "datetime" }
+
}
+
},
+
"viewNotFound": {
+
"type": "object",
+
"required": ["uri", "notFound"],
+
"properties": {
+
"uri": { "type": "string", "format": "at-uri" },
+
"notFound": { "type": "boolean", "const": true }
+
}
+
},
+
"viewBlocked": {
+
"type": "object",
+
"required": ["uri", "blocked", "author"],
+
"properties": {
+
"uri": { "type": "string", "format": "at-uri" },
+
"blocked": { "type": "boolean", "const": true },
+
"author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" }
+
}
+
},
+
"viewDetached": {
+
"type": "object",
+
"required": ["uri", "detached"],
+
"properties": {
+
"uri": { "type": "string", "format": "at-uri" },
+
"detached": { "type": "boolean", "const": true }
+
}
+
}
+
}
+
}
+43
crates/jacquard-lexicon/tests/fixtures/lexicons/recordWithMedia.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.embed.recordWithMedia",
+
"description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.",
+
"defs": {
+
"main": {
+
"type": "object",
+
"required": ["record", "media"],
+
"properties": {
+
"record": {
+
"type": "ref",
+
"ref": "app.bsky.embed.record"
+
},
+
"media": {
+
"type": "union",
+
"refs": [
+
"app.bsky.embed.images",
+
"app.bsky.embed.video",
+
"app.bsky.embed.external"
+
]
+
}
+
}
+
},
+
"view": {
+
"type": "object",
+
"required": ["record", "media"],
+
"properties": {
+
"record": {
+
"type": "ref",
+
"ref": "app.bsky.embed.record#view"
+
},
+
"media": {
+
"type": "union",
+
"refs": [
+
"app.bsky.embed.images#view",
+
"app.bsky.embed.video#view",
+
"app.bsky.embed.external#view"
+
]
+
}
+
}
+
}
+
}
+
}
+15
crates/jacquard-lexicon/tests/fixtures/lexicons/strongRef.json
···
···
+
{
+
"lexicon": 1,
+
"id": "com.atproto.repo.strongRef",
+
"description": "A URI with a content-hash fingerprint.",
+
"defs": {
+
"main": {
+
"type": "object",
+
"required": ["uri", "cid"],
+
"properties": {
+
"uri": { "type": "string", "format": "at-uri" },
+
"cid": { "type": "string", "format": "cid" }
+
}
+
}
+
}
+
}
+67
crates/jacquard-lexicon/tests/fixtures/lexicons/video.json
···
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.embed.video",
+
"description": "A video embedded in a Bluesky record (eg, a post).",
+
"defs": {
+
"main": {
+
"type": "object",
+
"required": ["video"],
+
"properties": {
+
"video": {
+
"type": "blob",
+
"description": "The mp4 video file. May be up to 100mb, formerly limited to 50mb.",
+
"accept": ["video/mp4"],
+
"maxSize": 100000000
+
},
+
"captions": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "#caption" },
+
"maxLength": 20
+
},
+
"alt": {
+
"type": "string",
+
"description": "Alt text description of the video, for accessibility.",
+
"maxGraphemes": 1000,
+
"maxLength": 10000
+
},
+
"aspectRatio": {
+
"type": "ref",
+
"ref": "app.bsky.embed.defs#aspectRatio"
+
}
+
}
+
},
+
"caption": {
+
"type": "object",
+
"required": ["lang", "file"],
+
"properties": {
+
"lang": {
+
"type": "string",
+
"format": "language"
+
},
+
"file": {
+
"type": "blob",
+
"accept": ["text/vtt"],
+
"maxSize": 20000
+
}
+
}
+
},
+
"view": {
+
"type": "object",
+
"required": ["cid", "playlist"],
+
"properties": {
+
"cid": { "type": "string", "format": "cid" },
+
"playlist": { "type": "string", "format": "uri" },
+
"thumbnail": { "type": "string", "format": "uri" },
+
"alt": {
+
"type": "string",
+
"maxGraphemes": 1000,
+
"maxLength": 10000
+
},
+
"aspectRatio": {
+
"type": "ref",
+
"ref": "app.bsky.embed.defs#aspectRatio"
+
}
+
}
+
}
+
}
+
}