From 19fc059bbb4c5407bff1d118239fcd27a7f5f060 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 29 Nov 2025 20:12:01 -0500 Subject: [PATCH] new lexicons, bunch of other build-out --- .../lexicons/sh_weaver_collab_accept.json | 35 + .../lexicons/sh_weaver_collab_defs.json | 18 + .../lexicons/sh_weaver_collab_invite.json | 60 ++ .../lexicons/sh_weaver_edit_diff.json | 4 + .../lexicons/sh_weaver_graph_bookmark.json | 35 + .../lexicons/sh_weaver_graph_defs.json | 14 + .../lexicons/sh_weaver_graph_follow.json | 29 + .../sh_weaver_graph_followAccept.json | 29 + .../lexicons/sh_weaver_graph_followGate.json | 33 + .../lexicons/sh_weaver_graph_like.json | 29 + .../lexicons/sh_weaver_graph_list.json | 56 ++ .../lexicons/sh_weaver_graph_listitem.json | 35 + .../lexicons/sh_weaver_graph_subscribe.json | 29 + .../sh_weaver_graph_subscribeAccept.json | 29 + .../lexicons/sh_weaver_notebook_book.json | 5 + .../lexicons/sh_weaver_notebook_entry.json | 5 + .../lexicons/sh_weaver_notebook_getEntry.json | 44 ++ .../sh_weaver_notebook_getEntryByTitle.json | 57 ++ .../sh_weaver_notebook_getNotebook.json | 46 ++ ...sh_weaver_notebook_getNotebookByTitle.json | 57 ++ crates/weaver-api/src/sh_weaver.rs | 2 + crates/weaver-api/src/sh_weaver/collab.rs | 63 ++ .../weaver-api/src/sh_weaver/collab/accept.rs | 376 +++++++++++ .../weaver-api/src/sh_weaver/collab/invite.rs | 634 ++++++++++++++++++ crates/weaver-api/src/sh_weaver/edit/diff.rs | 81 ++- crates/weaver-api/src/sh_weaver/graph.rs | 52 ++ .../src/sh_weaver/graph/bookmark.rs | 384 +++++++++++ .../weaver-api/src/sh_weaver/graph/follow.rs | 324 +++++++++ .../src/sh_weaver/graph/follow_accept.rs | 312 +++++++++ .../src/sh_weaver/graph/follow_gate.rs | 322 +++++++++ crates/weaver-api/src/sh_weaver/graph/like.rs | 312 +++++++++ crates/weaver-api/src/sh_weaver/graph/list.rs | 596 ++++++++++++++++ .../src/sh_weaver/graph/listitem.rs | 372 ++++++++++ .../src/sh_weaver/graph/subscribe.rs | 326 +++++++++ .../src/sh_weaver/graph/subscribe_accept.rs | 314 +++++++++ crates/weaver-api/src/sh_weaver/notebook.rs | 4 + .../weaver-api/src/sh_weaver/notebook/book.rs | 50 +- .../src/sh_weaver/notebook/entry.rs | 50 +- .../src/sh_weaver/notebook/get_entry.rs | 216 ++++++ .../sh_weaver/notebook/get_entry_by_title.rs | 236 +++++++ .../src/sh_weaver/notebook/get_notebook.rs | 156 +++++ .../notebook/get_notebook_by_title.rs | 226 +++++++ crates/weaver-app/{.env-example => .env-dev} | 0 crates/weaver-app/assets/styling/drafts.css | 102 +++ .../assets/styling/entry-actions.css | 91 +++ .../weaver-app/assets/styling/entry-card.css | 17 +- crates/weaver-app/assets/styling/entry.css | 8 + .../assets/styling/notebook-card.css | 46 +- .../assets/styling/notebook-cover.css | 19 + .../assets/styling/profile-actions.css | 82 +++ .../src/components/dialog/component.rs | 52 +- .../src/components/editor/component.rs | 45 +- crates/weaver-app/src/components/entry.rs | 232 ++++--- .../src/components/entry_actions.rs | 371 ++++++++++ crates/weaver-app/src/components/identity.rs | 159 +++-- crates/weaver-app/src/components/mod.rs | 5 + .../src/components/notebook_cover.rs | 30 +- .../src/components/profile_actions.rs | 101 +++ crates/weaver-app/src/data.rs | 86 ++- crates/weaver-app/src/main.rs | 24 +- crates/weaver-app/src/views/drafts.rs | 263 ++++++++ crates/weaver-app/src/views/editor.rs | 2 +- crates/weaver-app/src/views/mod.rs | 6 + crates/weaver-app/src/views/notebook.rs | 16 +- crates/weaver-common/src/agent.rs | 226 ++++--- lexicons/collab/accept.json | 31 + lexicons/collab/defs.json | 18 + lexicons/collab/invite.json | 56 ++ lexicons/edit/diff.json | 4 + lexicons/graph/bookmark.json | 32 + lexicons/graph/defs.json | 14 + lexicons/graph/follow.json | 26 + lexicons/graph/followAccept.json | 26 + lexicons/graph/followGate.json | 31 + lexicons/graph/like.json | 26 + lexicons/graph/list.json | 49 ++ lexicons/graph/listitem.json | 31 + lexicons/graph/subscribe.json | 26 + lexicons/graph/subscribeAccept.json | 26 + lexicons/notebook/book.json | 5 + lexicons/notebook/entry.json | 5 + lexicons/notebook/getEntry.json | 35 + lexicons/notebook/getEntryByTitle.json | 47 ++ lexicons/notebook/getNotebook.json | 41 ++ lexicons/notebook/getNotebookByTitle.json | 49 ++ 85 files changed, 8357 insertions(+), 261 deletions(-) create mode 100644 crates/weaver-api/lexicons/sh_weaver_collab_accept.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_collab_defs.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_collab_invite.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_bookmark.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_defs.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_follow.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_followAccept.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_followGate.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_like.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_list.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_listitem.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_subscribe.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_graph_subscribeAccept.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_notebook_getEntry.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_notebook_getEntryByTitle.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_notebook_getNotebook.json create mode 100644 crates/weaver-api/lexicons/sh_weaver_notebook_getNotebookByTitle.json create mode 100644 crates/weaver-api/src/sh_weaver/collab.rs create mode 100644 crates/weaver-api/src/sh_weaver/collab/accept.rs create mode 100644 crates/weaver-api/src/sh_weaver/collab/invite.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/bookmark.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/follow.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/follow_accept.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/follow_gate.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/like.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/list.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/listitem.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/subscribe.rs create mode 100644 crates/weaver-api/src/sh_weaver/graph/subscribe_accept.rs create mode 100644 crates/weaver-api/src/sh_weaver/notebook/get_entry.rs create mode 100644 crates/weaver-api/src/sh_weaver/notebook/get_entry_by_title.rs create mode 100644 crates/weaver-api/src/sh_weaver/notebook/get_notebook.rs create mode 100644 crates/weaver-api/src/sh_weaver/notebook/get_notebook_by_title.rs rename crates/weaver-app/{.env-example => .env-dev} (100%) create mode 100644 crates/weaver-app/assets/styling/drafts.css create mode 100644 crates/weaver-app/assets/styling/entry-actions.css create mode 100644 crates/weaver-app/assets/styling/profile-actions.css create mode 100644 crates/weaver-app/src/components/entry_actions.rs create mode 100644 crates/weaver-app/src/components/profile_actions.rs create mode 100644 crates/weaver-app/src/views/drafts.rs create mode 100644 lexicons/collab/accept.json create mode 100644 lexicons/collab/defs.json create mode 100644 lexicons/collab/invite.json create mode 100644 lexicons/graph/bookmark.json create mode 100644 lexicons/graph/defs.json create mode 100644 lexicons/graph/follow.json create mode 100644 lexicons/graph/followAccept.json create mode 100644 lexicons/graph/followGate.json create mode 100644 lexicons/graph/like.json create mode 100644 lexicons/graph/list.json create mode 100644 lexicons/graph/listitem.json create mode 100644 lexicons/graph/subscribe.json create mode 100644 lexicons/graph/subscribeAccept.json create mode 100644 lexicons/notebook/getEntry.json create mode 100644 lexicons/notebook/getEntryByTitle.json create mode 100644 lexicons/notebook/getNotebook.json create mode 100644 lexicons/notebook/getNotebookByTitle.json diff --git a/crates/weaver-api/lexicons/sh_weaver_collab_accept.json b/crates/weaver-api/lexicons/sh_weaver_collab_accept.json new file mode 100644 index 0000000..da159b6 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_collab_accept.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "sh.weaver.collab.accept", + "defs": { + "main": { + "type": "record", + "description": "Acceptance of a collaboration invite. Completes the two-way agreement.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "invite", + "resource", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "invite": { + "type": "ref", + "description": "Reference to the invite record being accepted.", + "ref": "com.atproto.repo.strongRef" + }, + "resource": { + "type": "string", + "description": "URI of the resource (denormalized for easier querying).", + "format": "at-uri" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_collab_defs.json b/crates/weaver-api/lexicons/sh_weaver_collab_defs.json new file mode 100644 index 0000000..f273a93 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_collab_defs.json @@ -0,0 +1,18 @@ +{ + "lexicon": 1, + "id": "sh.weaver.collab.defs", + "defs": { + "chapter": { + "type": "token", + "description": "Collaboration scoped to a chapter." + }, + "entry": { + "type": "token", + "description": "Collaboration scoped to a single entry." + }, + "notebook": { + "type": "token", + "description": "Collaboration scoped to an entire notebook." + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_collab_invite.json b/crates/weaver-api/lexicons/sh_weaver_collab_invite.json new file mode 100644 index 0000000..a08047b --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_collab_invite.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "sh.weaver.collab.invite", + "defs": { + "collabScope": { + "type": "string", + "description": "The scope/type of collaboration.", + "knownValues": [ + "sh.weaver.collab.defs#notebook", + "sh.weaver.collab.defs#entry", + "sh.weaver.collab.defs#chapter" + ] + }, + "main": { + "type": "record", + "description": "Invitation to collaborate on a resource (notebook, entry, chapter, etc.). Creates half of a two-way agreement.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "resource", + "invitee", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "expiresAt": { + "type": "string", + "description": "Optional expiration for the invite.", + "format": "datetime" + }, + "invitee": { + "type": "string", + "description": "DID of the user being invited.", + "format": "did" + }, + "message": { + "type": "string", + "description": "Optional message to the invitee.", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "resource": { + "type": "ref", + "description": "The resource to collaborate on (notebook, entry, chapter, etc.).", + "ref": "com.atproto.repo.strongRef" + }, + "scope": { + "type": "ref", + "description": "Optional explicit scope type. If omitted, inferred from resource lexicon.", + "ref": "#collabScope" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_edit_diff.json b/crates/weaver-api/lexicons/sh_weaver_edit_diff.json index 683833d..1ecbb7c 100644 --- a/crates/weaver-api/lexicons/sh_weaver_edit_diff.json +++ b/crates/weaver-api/lexicons/sh_weaver_edit_diff.json @@ -13,6 +13,10 @@ "doc" ], "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, "doc": { "type": "ref", "ref": "sh.weaver.edit.defs#docRef" diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_bookmark.json b/crates/weaver-api/lexicons/sh_weaver_graph_bookmark.json new file mode 100644 index 0000000..a442c41 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_bookmark.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.bookmark", + "defs": { + "main": { + "type": "record", + "description": "Bookmark a notebook or entry for later reading.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "note": { + "type": "string", + "description": "Optional private note about why you saved this.", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "subject": { + "type": "ref", + "description": "The notebook or entry being bookmarked.", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_defs.json b/crates/weaver-api/lexicons/sh_weaver_graph_defs.json new file mode 100644 index 0000000..ed6a33e --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_defs.json @@ -0,0 +1,14 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.defs", + "defs": { + "curatelist": { + "type": "token", + "description": "A curated collection of notebooks/entries for sharing." + }, + "readinglist": { + "type": "token", + "description": "A personal reading list." + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_follow.json b/crates/weaver-api/lexicons/sh_weaver_graph_follow.json new file mode 100644 index 0000000..8e68b33 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_follow.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.follow", + "defs": { + "main": { + "type": "record", + "description": "Request to follow an author. Requires acceptance to be active.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "subject": { + "type": "string", + "description": "DID of the author to follow.", + "format": "did" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_followAccept.json b/crates/weaver-api/lexicons/sh_weaver_graph_followAccept.json new file mode 100644 index 0000000..9fe95de --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_followAccept.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.followAccept", + "defs": { + "main": { + "type": "record", + "description": "Acceptance of a follow request. Completes the two-way agreement.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "follow", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "follow": { + "type": "ref", + "description": "Reference to the follow record being accepted.", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_followGate.json b/crates/weaver-api/lexicons/sh_weaver_graph_followGate.json new file mode 100644 index 0000000..71b330e --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_followGate.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.followGate", + "defs": { + "main": { + "type": "record", + "description": "Settings controlling follow approval behavior. Absence means auto-accept.", + "key": "self", + "record": { + "type": "object", + "required": [ + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "invalidatePrior": { + "type": "boolean", + "description": "If true, previously auto-accepted follows are invalidated when requireApproval is enabled. Appview should treat followAccept records created before this gate's createdAt as invalid.", + "default": false + }, + "requireApproval": { + "type": "boolean", + "description": "If true, follows require manual acceptance.", + "default": false + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_like.json b/crates/weaver-api/lexicons/sh_weaver_graph_like.json new file mode 100644 index 0000000..c21af69 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_like.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.like", + "defs": { + "main": { + "type": "record", + "description": "Record declaring a 'like' of a notebook or entry.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "subject": { + "type": "ref", + "description": "The notebook or entry being liked.", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_list.json b/crates/weaver-api/lexicons/sh_weaver_graph_list.json new file mode 100644 index 0000000..ee56c87 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_list.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.list", + "defs": { + "listPurpose": { + "type": "string", + "knownValues": [ + "sh.weaver.graph.defs#curatelist", + "sh.weaver.graph.defs#readinglist" + ] + }, + "main": { + "type": "record", + "description": "A curated list of notebooks and/or entries.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "name", + "purpose", + "createdAt" + ], + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "name": { + "type": "string", + "description": "Display name for the list.", + "minLength": 1, + "maxLength": 64 + }, + "purpose": { + "type": "ref", + "description": "The purpose/type of list.", + "ref": "#listPurpose" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_listitem.json b/crates/weaver-api/lexicons/sh_weaver_graph_listitem.json new file mode 100644 index 0000000..2da4eb4 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_listitem.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.listitem", + "defs": { + "main": { + "type": "record", + "description": "An item in a list.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "subject", + "list", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "list": { + "type": "string", + "description": "Reference to the list record.", + "format": "at-uri" + }, + "subject": { + "type": "ref", + "description": "The notebook or entry being added to the list.", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_subscribe.json b/crates/weaver-api/lexicons/sh_weaver_graph_subscribe.json new file mode 100644 index 0000000..a509b73 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_subscribe.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.subscribe", + "defs": { + "main": { + "type": "record", + "description": "Request to subscribe to a notebook. Requires acceptance to be active.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "notebook", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "notebook": { + "type": "string", + "description": "URI of the notebook to subscribe to.", + "format": "at-uri" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_graph_subscribeAccept.json b/crates/weaver-api/lexicons/sh_weaver_graph_subscribeAccept.json new file mode 100644 index 0000000..9110086 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_graph_subscribeAccept.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.subscribeAccept", + "defs": { + "main": { + "type": "record", + "description": "Acceptance of a subscription request.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "subscribe", + "createdAt" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "subscribe": { + "type": "ref", + "description": "Reference to the subscribe record being accepted.", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_book.json b/crates/weaver-api/lexicons/sh_weaver_notebook_book.json index d078894..a79baf8 100644 --- a/crates/weaver-api/lexicons/sh_weaver_notebook_book.json +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_book.json @@ -47,6 +47,11 @@ "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" + }, + "updatedAt": { + "type": "string", + "description": "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios.", + "format": "datetime" } } } diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json b/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json index 272dac9..4088a6e 100644 --- a/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json @@ -65,6 +65,11 @@ "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" + }, + "updatedAt": { + "type": "string", + "description": "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios.", + "format": "datetime" } } } diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_getEntry.json b/crates/weaver-api/lexicons/sh_weaver_notebook_getEntry.json new file mode 100644 index 0000000..715c0d0 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_getEntry.json @@ -0,0 +1,44 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getEntry", + "defs": { + "main": { + "type": "query", + "description": "Get an entry view by notebook URI and index, including prev/next navigation.", + "parameters": { + "type": "params", + "required": [ + "notebook" + ], + "properties": { + "index": { + "type": "integer", + "description": "Zero-based index of the entry in the notebook's entry list.", + "default": 0, + "minimum": 0 + }, + "notebook": { + "type": "string", + "description": "AT-URI of the notebook containing the entry.", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#bookEntryView" + } + }, + "errors": [ + { + "name": "NotebookNotFound" + }, + { + "name": "EntryNotFound" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_getEntryByTitle.json b/crates/weaver-api/lexicons/sh_weaver_notebook_getEntryByTitle.json new file mode 100644 index 0000000..bcc1f5e --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_getEntryByTitle.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getEntryByTitle", + "defs": { + "main": { + "type": "query", + "description": "Get an entry view by notebook URI and title. Matches on either the entry's path or title field.", + "parameters": { + "type": "params", + "required": [ + "notebook", + "title" + ], + "properties": { + "notebook": { + "type": "string", + "description": "AT-URI of the notebook containing the entry.", + "format": "at-uri" + }, + "title": { + "type": "string", + "description": "Title or path of the entry to fetch.", + "maxLength": 300 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "entry", + "record" + ], + "properties": { + "entry": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#bookEntryView" + }, + "record": { + "type": "unknown", + "description": "The raw entry record data." + } + } + } + }, + "errors": [ + { + "name": "NotebookNotFound" + }, + { + "name": "EntryNotFound" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_getNotebook.json b/crates/weaver-api/lexicons/sh_weaver_notebook_getNotebook.json new file mode 100644 index 0000000..a66a1c2 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_getNotebook.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getNotebook", + "defs": { + "main": { + "type": "query", + "description": "Get a notebook view by its AT-URI, including hydrated author profiles and entry list.", + "parameters": { + "type": "params", + "required": [ + "notebook" + ], + "properties": { + "notebook": { + "type": "string", + "description": "AT-URI of the notebook to fetch.", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "notebook", + "entries" + ], + "properties": { + "entries": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + }, + "notebook": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#notebookView" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_getNotebookByTitle.json b/crates/weaver-api/lexicons/sh_weaver_notebook_getNotebookByTitle.json new file mode 100644 index 0000000..f572a31 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_getNotebookByTitle.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getNotebookByTitle", + "defs": { + "main": { + "type": "query", + "description": "Get a notebook view by actor and title. Matches on either the notebook's path or title field.", + "parameters": { + "type": "params", + "required": [ + "actor", + "title" + ], + "properties": { + "actor": { + "type": "string", + "description": "Handle or DID of the notebook owner.", + "format": "at-identifier" + }, + "title": { + "type": "string", + "description": "Title or path of the notebook to fetch.", + "maxLength": 300 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "notebook", + "entries" + ], + "properties": { + "entries": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + }, + "notebook": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#notebookView" + } + } + } + }, + "errors": [ + { + "name": "NotebookNotFound" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver.rs b/crates/weaver-api/src/sh_weaver.rs index 4d95cde..7ff2610 100644 --- a/crates/weaver-api/src/sh_weaver.rs +++ b/crates/weaver-api/src/sh_weaver.rs @@ -4,7 +4,9 @@ // Any manual changes will be overwritten on the next regeneration. pub mod actor; +pub mod collab; pub mod edit; pub mod embed; +pub mod graph; pub mod notebook; pub mod publish; \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/collab.rs b/crates/weaver-api/src/sh_weaver/collab.rs new file mode 100644 index 0000000..75c6cda --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/collab.rs @@ -0,0 +1,63 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.collab.defs +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +pub mod accept; +pub mod invite; + +/// Collaboration scoped to a chapter. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Hash, + jacquard_derive::IntoStatic +)] +pub struct Chapter; +impl std::fmt::Display for Chapter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "chapter") + } +} + +/// Collaboration scoped to a single entry. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Hash, + jacquard_derive::IntoStatic +)] +pub struct Entry; +impl std::fmt::Display for Entry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "entry") + } +} + +/// Collaboration scoped to an entire notebook. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Hash, + jacquard_derive::IntoStatic +)] +pub struct Notebook; +impl std::fmt::Display for Notebook { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "notebook") + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/collab/accept.rs b/crates/weaver-api/src/sh_weaver/collab/accept.rs new file mode 100644 index 0000000..d347076 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/collab/accept.rs @@ -0,0 +1,376 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.collab.accept +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Acceptance of a collaboration invite. Completes the two-way agreement. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Accept<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// Reference to the invite record being accepted. + #[serde(borrow)] + pub invite: crate::com_atproto::repo::strong_ref::StrongRef<'a>, + /// URI of the resource (denormalized for easier querying). + #[serde(borrow)] + pub resource: jacquard_common::types::string::AtUri<'a>, +} + +pub mod accept_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 Invite; + type Resource; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Invite = Unset; + type Resource = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `invite` field to Set + pub struct SetInvite(PhantomData S>); + impl sealed::Sealed for SetInvite {} + impl State for SetInvite { + type Invite = Set; + type Resource = S::Resource; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `resource` field to Set + pub struct SetResource(PhantomData S>); + impl sealed::Sealed for SetResource {} + impl State for SetResource { + type Invite = S::Invite; + type Resource = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Invite = S::Invite; + type Resource = S::Resource; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `invite` field + pub struct invite(()); + ///Marker type for the `resource` field + pub struct resource(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct AcceptBuilder<'a, S: accept_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> Accept<'a> { + /// Create a new builder for this type + pub fn new() -> AcceptBuilder<'a, accept_state::Empty> { + AcceptBuilder::new() + } +} + +impl<'a> AcceptBuilder<'a, accept_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + AcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> AcceptBuilder<'a, S> +where + S: accept_state::State, + S::CreatedAt: accept_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> AcceptBuilder<'a, accept_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + AcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> AcceptBuilder<'a, S> +where + S: accept_state::State, + S::Invite: accept_state::IsUnset, +{ + /// Set the `invite` field (required) + pub fn invite( + mut self, + value: impl Into>, + ) -> AcceptBuilder<'a, accept_state::SetInvite> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + AcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> AcceptBuilder<'a, S> +where + S: accept_state::State, + S::Resource: accept_state::IsUnset, +{ + /// Set the `resource` field (required) + pub fn resource( + mut self, + value: impl Into>, + ) -> AcceptBuilder<'a, accept_state::SetResource> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + AcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> AcceptBuilder<'a, S> +where + S: accept_state::State, + S::Invite: accept_state::IsSet, + S::Resource: accept_state::IsSet, + S::CreatedAt: accept_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Accept<'a> { + Accept { + created_at: self.__unsafe_private_named.0.unwrap(), + invite: self.__unsafe_private_named.1.unwrap(), + resource: 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>, + >, + ) -> Accept<'a> { + Accept { + created_at: self.__unsafe_private_named.0.unwrap(), + invite: self.__unsafe_private_named.1.unwrap(), + resource: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> Accept<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, AcceptRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct AcceptGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: Accept<'a>, +} + +impl From> for Accept<'_> { + fn from(output: AcceptGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for Accept<'_> { + const NSID: &'static str = "sh.weaver.collab.accept"; + type Record = AcceptRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct AcceptRecord; +impl jacquard_common::xrpc::XrpcResp for AcceptRecord { + const NSID: &'static str = "sh.weaver.collab.accept"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = AcceptGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for AcceptRecord { + const NSID: &'static str = "sh.weaver.collab.accept"; + type Record = AcceptRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Accept<'a> { + fn nsid() -> &'static str { + "sh.weaver.collab.accept" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_collab_accept() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_collab_accept() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.collab.accept"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Acceptance of a collaboration invite. Completes the two-way agreement.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("invite"), + ::jacquard_common::smol_str::SmolStr::new_static("resource"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("invite"), + ::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( + "resource", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "URI of the resource (denormalized for easier querying).", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::AtUri, + ), + 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 + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/collab/invite.rs b/crates/weaver-api/src/sh_weaver/collab/invite.rs new file mode 100644 index 0000000..549f9fd --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/collab/invite.rs @@ -0,0 +1,634 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.collab.invite +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// The scope/type of collaboration. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CollabScope<'a> { + ShWeaverCollabDefsNotebook, + ShWeaverCollabDefsEntry, + ShWeaverCollabDefsChapter, + Other(jacquard_common::CowStr<'a>), +} + +impl<'a> CollabScope<'a> { + pub fn as_str(&self) -> &str { + match self { + Self::ShWeaverCollabDefsNotebook => "sh.weaver.collab.defs#notebook", + Self::ShWeaverCollabDefsEntry => "sh.weaver.collab.defs#entry", + Self::ShWeaverCollabDefsChapter => "sh.weaver.collab.defs#chapter", + Self::Other(s) => s.as_ref(), + } + } +} + +impl<'a> From<&'a str> for CollabScope<'a> { + fn from(s: &'a str) -> Self { + match s { + "sh.weaver.collab.defs#notebook" => Self::ShWeaverCollabDefsNotebook, + "sh.weaver.collab.defs#entry" => Self::ShWeaverCollabDefsEntry, + "sh.weaver.collab.defs#chapter" => Self::ShWeaverCollabDefsChapter, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> From for CollabScope<'a> { + fn from(s: String) -> Self { + match s.as_str() { + "sh.weaver.collab.defs#notebook" => Self::ShWeaverCollabDefsNotebook, + "sh.weaver.collab.defs#entry" => Self::ShWeaverCollabDefsEntry, + "sh.weaver.collab.defs#chapter" => Self::ShWeaverCollabDefsChapter, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> AsRef for CollabScope<'a> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl<'a> serde::Serialize for CollabScope<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de, 'a> serde::Deserialize<'de> for CollabScope<'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 CollabScope<'_> { + type Output = CollabScope<'static>; + fn into_static(self) -> Self::Output { + match self { + CollabScope::ShWeaverCollabDefsNotebook => { + CollabScope::ShWeaverCollabDefsNotebook + } + CollabScope::ShWeaverCollabDefsEntry => CollabScope::ShWeaverCollabDefsEntry, + CollabScope::ShWeaverCollabDefsChapter => { + CollabScope::ShWeaverCollabDefsChapter + } + CollabScope::Other(v) => CollabScope::Other(v.into_static()), + } + } +} + +/// Invitation to collaborate on a resource (notebook, entry, chapter, etc.). Creates half of a two-way agreement. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Invite<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// Optional expiration for the invite. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub expires_at: std::option::Option, + /// DID of the user being invited. + #[serde(borrow)] + pub invitee: jacquard_common::types::string::Did<'a>, + /// Optional message to the invitee. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub message: std::option::Option>, + /// The resource to collaborate on (notebook, entry, chapter, etc.). + #[serde(borrow)] + pub resource: crate::com_atproto::repo::strong_ref::StrongRef<'a>, + /// Optional explicit scope type. If omitted, inferred from resource lexicon. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub scope: std::option::Option>, +} + +pub mod invite_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 Resource; + type Invitee; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Resource = Unset; + type Invitee = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `resource` field to Set + pub struct SetResource(PhantomData S>); + impl sealed::Sealed for SetResource {} + impl State for SetResource { + type Resource = Set; + type Invitee = S::Invitee; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `invitee` field to Set + pub struct SetInvitee(PhantomData S>); + impl sealed::Sealed for SetInvitee {} + impl State for SetInvitee { + type Resource = S::Resource; + type Invitee = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Resource = S::Resource; + type Invitee = S::Invitee; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `resource` field + pub struct resource(()); + ///Marker type for the `invitee` field + pub struct invitee(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct InviteBuilder<'a, S: invite_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>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> Invite<'a> { + /// Create a new builder for this type + pub fn new() -> InviteBuilder<'a, invite_state::Empty> { + InviteBuilder::new() + } +} + +impl<'a> InviteBuilder<'a, invite_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + InviteBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None, None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> InviteBuilder<'a, S> +where + S: invite_state::State, + S::CreatedAt: invite_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> InviteBuilder<'a, invite_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + InviteBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: invite_state::State> InviteBuilder<'a, S> { + /// Set the `expiresAt` field (optional) + pub fn expires_at( + mut self, + value: impl Into>, + ) -> Self { + self.__unsafe_private_named.1 = value.into(); + self + } + /// Set the `expiresAt` field to an Option value (optional) + pub fn maybe_expires_at( + mut self, + value: Option, + ) -> Self { + self.__unsafe_private_named.1 = value; + self + } +} + +impl<'a, S> InviteBuilder<'a, S> +where + S: invite_state::State, + S::Invitee: invite_state::IsUnset, +{ + /// Set the `invitee` field (required) + pub fn invitee( + mut self, + value: impl Into>, + ) -> InviteBuilder<'a, invite_state::SetInvitee> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + InviteBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: invite_state::State> InviteBuilder<'a, S> { + /// Set the `message` field (optional) + pub fn message( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.3 = value.into(); + self + } + /// Set the `message` field to an Option value (optional) + pub fn maybe_message(mut self, value: Option>) -> Self { + self.__unsafe_private_named.3 = value; + self + } +} + +impl<'a, S> InviteBuilder<'a, S> +where + S: invite_state::State, + S::Resource: invite_state::IsUnset, +{ + /// Set the `resource` field (required) + pub fn resource( + mut self, + value: impl Into>, + ) -> InviteBuilder<'a, invite_state::SetResource> { + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); + InviteBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: invite_state::State> InviteBuilder<'a, S> { + /// Set the `scope` field (optional) + pub fn scope( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.5 = value.into(); + self + } + /// Set the `scope` field to an Option value (optional) + pub fn maybe_scope( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.5 = value; + self + } +} + +impl<'a, S> InviteBuilder<'a, S> +where + S: invite_state::State, + S::Resource: invite_state::IsSet, + S::Invitee: invite_state::IsSet, + S::CreatedAt: invite_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Invite<'a> { + Invite { + created_at: self.__unsafe_private_named.0.unwrap(), + expires_at: self.__unsafe_private_named.1, + invitee: self.__unsafe_private_named.2.unwrap(), + message: self.__unsafe_private_named.3, + resource: self.__unsafe_private_named.4.unwrap(), + scope: self.__unsafe_private_named.5, + 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>, + >, + ) -> Invite<'a> { + Invite { + created_at: self.__unsafe_private_named.0.unwrap(), + expires_at: self.__unsafe_private_named.1, + invitee: self.__unsafe_private_named.2.unwrap(), + message: self.__unsafe_private_named.3, + resource: self.__unsafe_private_named.4.unwrap(), + scope: self.__unsafe_private_named.5, + extra_data: Some(extra_data), + } + } +} + +impl<'a> Invite<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, InviteRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct InviteGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: Invite<'a>, +} + +impl From> for Invite<'_> { + fn from(output: InviteGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for Invite<'_> { + const NSID: &'static str = "sh.weaver.collab.invite"; + type Record = InviteRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct InviteRecord; +impl jacquard_common::xrpc::XrpcResp for InviteRecord { + const NSID: &'static str = "sh.weaver.collab.invite"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = InviteGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for InviteRecord { + const NSID: &'static str = "sh.weaver.collab.invite"; + type Record = InviteRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Invite<'a> { + fn nsid() -> &'static str { + "sh.weaver.collab.invite" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_collab_invite() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + if let Some(ref value) = self.message { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 3000usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "message", + ), + max: 3000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.message { + { + let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( + value.as_ref(), + true, + ) + .count(); + if count > 300usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxGraphemes { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "message", + ), + max: 300usize, + actual: count, + }); + } + } + } + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_collab_invite() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.collab.invite"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("collabScope"), + ::jacquard_lexicon::lexicon::LexUserType::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "The scope/type of collaboration.", + ), + ), + 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("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Invitation to collaborate on a resource (notebook, entry, chapter, etc.). Creates half of a two-way agreement.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("resource"), + ::jacquard_common::smol_str::SmolStr::new_static("invitee"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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( + "expiresAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Optional expiration for the invite.", + ), + ), + 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("invitee"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "DID of the user being invited.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Did, + ), + 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("message"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Optional message to the invitee.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: Some(3000usize), + min_graphemes: None, + max_graphemes: Some(300usize), + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "resource", + ), + ::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("scope"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static("#collabScope"), + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/edit/diff.rs b/crates/weaver-api/src/sh_weaver/edit/diff.rs index 9937883..cce32ea 100644 --- a/crates/weaver-api/src/sh_weaver/edit/diff.rs +++ b/crates/weaver-api/src/sh_weaver/edit/diff.rs @@ -18,6 +18,8 @@ )] #[serde(rename_all = "camelCase")] pub struct Diff<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub created_at: std::option::Option, #[serde(borrow)] pub doc: crate::sh_weaver::edit::DocRef<'a>, /// An inline diff for for small edit batches. Either this or snapshot must be present to be valid @@ -82,6 +84,7 @@ pub mod diff_state { pub struct DiffBuilder<'a, S: diff_state::State> { _phantom_state: ::core::marker::PhantomData S>, __unsafe_private_named: ( + ::core::option::Option, ::core::option::Option>, ::core::option::Option, ::core::option::Option>, @@ -103,12 +106,31 @@ impl<'a> DiffBuilder<'a, diff_state::Empty> { pub fn new() -> Self { DiffBuilder { _phantom_state: ::core::marker::PhantomData, - __unsafe_private_named: (None, None, None, None, None), + __unsafe_private_named: (None, None, None, None, None, None), _phantom: ::core::marker::PhantomData, } } } +impl<'a, S: diff_state::State> DiffBuilder<'a, S> { + /// Set the `createdAt` field (optional) + pub fn created_at( + mut self, + value: impl Into>, + ) -> Self { + self.__unsafe_private_named.0 = value.into(); + self + } + /// Set the `createdAt` field to an Option value (optional) + pub fn maybe_created_at( + mut self, + value: Option, + ) -> Self { + self.__unsafe_private_named.0 = value; + self + } +} + impl<'a, S> DiffBuilder<'a, S> where S: diff_state::State, @@ -119,7 +141,7 @@ where mut self, value: impl Into>, ) -> DiffBuilder<'a, diff_state::SetDoc> { - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); DiffBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -131,12 +153,12 @@ where impl<'a, S: diff_state::State> DiffBuilder<'a, S> { /// Set the `inlineDiff` field (optional) pub fn inline_diff(mut self, value: impl Into>) -> Self { - self.__unsafe_private_named.1 = value.into(); + self.__unsafe_private_named.2 = value.into(); self } /// Set the `inlineDiff` field to an Option value (optional) pub fn maybe_inline_diff(mut self, value: Option) -> Self { - self.__unsafe_private_named.1 = value; + self.__unsafe_private_named.2 = value; self } } @@ -147,7 +169,7 @@ impl<'a, S: diff_state::State> DiffBuilder<'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 `prev` field to an Option value (optional) @@ -155,7 +177,7 @@ impl<'a, S: diff_state::State> DiffBuilder<'a, S> { mut self, value: Option>, ) -> Self { - self.__unsafe_private_named.2 = value; + self.__unsafe_private_named.3 = value; self } } @@ -170,7 +192,7 @@ where mut self, value: impl Into>, ) -> DiffBuilder<'a, diff_state::SetRoot> { - self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); DiffBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -185,7 +207,7 @@ impl<'a, S: diff_state::State> DiffBuilder<'a, S> { mut self, value: impl Into>>, ) -> Self { - self.__unsafe_private_named.4 = value.into(); + self.__unsafe_private_named.5 = value.into(); self } /// Set the `snapshot` field to an Option value (optional) @@ -193,7 +215,7 @@ impl<'a, S: diff_state::State> DiffBuilder<'a, S> { mut self, value: Option>, ) -> Self { - self.__unsafe_private_named.4 = value; + self.__unsafe_private_named.5 = value; self } } @@ -207,11 +229,12 @@ where /// Build the final struct pub fn build(self) -> Diff<'a> { Diff { - doc: self.__unsafe_private_named.0.unwrap(), - inline_diff: self.__unsafe_private_named.1, - prev: self.__unsafe_private_named.2, - root: self.__unsafe_private_named.3.unwrap(), - snapshot: self.__unsafe_private_named.4, + created_at: self.__unsafe_private_named.0, + doc: self.__unsafe_private_named.1.unwrap(), + inline_diff: self.__unsafe_private_named.2, + prev: self.__unsafe_private_named.3, + root: self.__unsafe_private_named.4.unwrap(), + snapshot: self.__unsafe_private_named.5, extra_data: Default::default(), } } @@ -224,11 +247,12 @@ where >, ) -> Diff<'a> { Diff { - doc: self.__unsafe_private_named.0.unwrap(), - inline_diff: self.__unsafe_private_named.1, - prev: self.__unsafe_private_named.2, - root: self.__unsafe_private_named.3.unwrap(), - snapshot: self.__unsafe_private_named.4, + created_at: self.__unsafe_private_named.0, + doc: self.__unsafe_private_named.1.unwrap(), + inline_diff: self.__unsafe_private_named.2, + prev: self.__unsafe_private_named.3, + root: self.__unsafe_private_named.4.unwrap(), + snapshot: self.__unsafe_private_named.5, extra_data: Some(extra_data), } } @@ -343,6 +367,25 @@ fn lexicon_doc_sh_weaver_edit_diff() -> ::jacquard_lexicon::lexicon::LexiconDoc< properties: { #[allow(unused_mut)] let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("doc"), ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { diff --git a/crates/weaver-api/src/sh_weaver/graph.rs b/crates/weaver-api/src/sh_weaver/graph.rs new file mode 100644 index 0000000..5746d0c --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph.rs @@ -0,0 +1,52 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.defs +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +pub mod bookmark; +pub mod follow; +pub mod follow_accept; +pub mod follow_gate; +pub mod like; +pub mod list; +pub mod listitem; +pub mod subscribe; +pub mod subscribe_accept; + +/// A curated collection of notebooks/entries for sharing. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Hash, + jacquard_derive::IntoStatic +)] +pub struct Curatelist; +impl std::fmt::Display for Curatelist { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "curatelist") + } +} + +/// A personal reading list. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Hash, + jacquard_derive::IntoStatic +)] +pub struct Readinglist; +impl std::fmt::Display for Readinglist { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "readinglist") + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/bookmark.rs b/crates/weaver-api/src/sh_weaver/graph/bookmark.rs new file mode 100644 index 0000000..3021db2 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/bookmark.rs @@ -0,0 +1,384 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.bookmark +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Bookmark a notebook or entry for later reading. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Bookmark<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// Optional private note about why you saved this. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub note: std::option::Option>, + /// The notebook or entry being bookmarked. + #[serde(borrow)] + pub subject: crate::com_atproto::repo::strong_ref::StrongRef<'a>, +} + +pub mod bookmark_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 Subject; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subject = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `subject` field to Set + pub struct SetSubject(PhantomData S>); + impl sealed::Sealed for SetSubject {} + impl State for SetSubject { + type Subject = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Subject = S::Subject; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subject` field + pub struct subject(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct BookmarkBuilder<'a, S: bookmark_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> Bookmark<'a> { + /// Create a new builder for this type + pub fn new() -> BookmarkBuilder<'a, bookmark_state::Empty> { + BookmarkBuilder::new() + } +} + +impl<'a> BookmarkBuilder<'a, bookmark_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + BookmarkBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> BookmarkBuilder<'a, S> +where + S: bookmark_state::State, + S::CreatedAt: bookmark_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> BookmarkBuilder<'a, bookmark_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + BookmarkBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: bookmark_state::State> BookmarkBuilder<'a, S> { + /// Set the `note` field (optional) + pub fn note( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.1 = value.into(); + self + } + /// Set the `note` field to an Option value (optional) + pub fn maybe_note(mut self, value: Option>) -> Self { + self.__unsafe_private_named.1 = value; + self + } +} + +impl<'a, S> BookmarkBuilder<'a, S> +where + S: bookmark_state::State, + S::Subject: bookmark_state::IsUnset, +{ + /// Set the `subject` field (required) + pub fn subject( + mut self, + value: impl Into>, + ) -> BookmarkBuilder<'a, bookmark_state::SetSubject> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + BookmarkBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> BookmarkBuilder<'a, S> +where + S: bookmark_state::State, + S::Subject: bookmark_state::IsSet, + S::CreatedAt: bookmark_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Bookmark<'a> { + Bookmark { + created_at: self.__unsafe_private_named.0.unwrap(), + note: self.__unsafe_private_named.1, + subject: 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>, + >, + ) -> Bookmark<'a> { + Bookmark { + created_at: self.__unsafe_private_named.0.unwrap(), + note: self.__unsafe_private_named.1, + subject: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> Bookmark<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, BookmarkRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct BookmarkGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: Bookmark<'a>, +} + +impl From> for Bookmark<'_> { + fn from(output: BookmarkGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for Bookmark<'_> { + const NSID: &'static str = "sh.weaver.graph.bookmark"; + type Record = BookmarkRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct BookmarkRecord; +impl jacquard_common::xrpc::XrpcResp for BookmarkRecord { + const NSID: &'static str = "sh.weaver.graph.bookmark"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = BookmarkGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for BookmarkRecord { + const NSID: &'static str = "sh.weaver.graph.bookmark"; + type Record = BookmarkRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Bookmark<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.bookmark" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_bookmark() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + if let Some(ref value) = self.note { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 3000usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "note", + ), + max: 3000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.note { + { + let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( + value.as_ref(), + true, + ) + .count(); + if count > 300usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxGraphemes { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "note", + ), + max: 300usize, + actual: count, + }); + } + } + } + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_bookmark() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.bookmark"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Bookmark a notebook or entry for later reading.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("subject"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("note"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Optional private note about why you saved this.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: Some(3000usize), + min_graphemes: None, + max_graphemes: Some(300usize), + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("subject"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", + ), + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/follow.rs b/crates/weaver-api/src/sh_weaver/graph/follow.rs new file mode 100644 index 0000000..c20dc16 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/follow.rs @@ -0,0 +1,324 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.follow +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Request to follow an author. Requires acceptance to be active. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Follow<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// DID of the author to follow. + #[serde(borrow)] + pub subject: jacquard_common::types::string::Did<'a>, +} + +pub mod follow_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 Subject; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subject = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `subject` field to Set + pub struct SetSubject(PhantomData S>); + impl sealed::Sealed for SetSubject {} + impl State for SetSubject { + type Subject = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Subject = S::Subject; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subject` field + pub struct subject(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct FollowBuilder<'a, S: follow_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> Follow<'a> { + /// Create a new builder for this type + pub fn new() -> FollowBuilder<'a, follow_state::Empty> { + FollowBuilder::new() + } +} + +impl<'a> FollowBuilder<'a, follow_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + FollowBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FollowBuilder<'a, S> +where + S: follow_state::State, + S::CreatedAt: follow_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> FollowBuilder<'a, follow_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + FollowBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FollowBuilder<'a, S> +where + S: follow_state::State, + S::Subject: follow_state::IsUnset, +{ + /// Set the `subject` field (required) + pub fn subject( + mut self, + value: impl Into>, + ) -> FollowBuilder<'a, follow_state::SetSubject> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + FollowBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FollowBuilder<'a, S> +where + S: follow_state::State, + S::Subject: follow_state::IsSet, + S::CreatedAt: follow_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Follow<'a> { + Follow { + created_at: self.__unsafe_private_named.0.unwrap(), + subject: 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>, + >, + ) -> Follow<'a> { + Follow { + created_at: self.__unsafe_private_named.0.unwrap(), + subject: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> Follow<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, FollowRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct FollowGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: Follow<'a>, +} + +impl From> for Follow<'_> { + fn from(output: FollowGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for Follow<'_> { + const NSID: &'static str = "sh.weaver.graph.follow"; + type Record = FollowRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct FollowRecord; +impl jacquard_common::xrpc::XrpcResp for FollowRecord { + const NSID: &'static str = "sh.weaver.graph.follow"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = FollowGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for FollowRecord { + const NSID: &'static str = "sh.weaver.graph.follow"; + type Record = FollowRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Follow<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.follow" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_follow() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_follow() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.follow"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Request to follow an author. Requires acceptance to be active.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("subject"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("subject"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "DID of the author to follow.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Did, + ), + 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 + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/follow_accept.rs b/crates/weaver-api/src/sh_weaver/graph/follow_accept.rs new file mode 100644 index 0000000..f92a05f --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/follow_accept.rs @@ -0,0 +1,312 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.followAccept +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Acceptance of a follow request. Completes the two-way agreement. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct FollowAccept<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// Reference to the follow record being accepted. + #[serde(borrow)] + pub follow: crate::com_atproto::repo::strong_ref::StrongRef<'a>, +} + +pub mod follow_accept_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 Follow; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Follow = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `follow` field to Set + pub struct SetFollow(PhantomData S>); + impl sealed::Sealed for SetFollow {} + impl State for SetFollow { + type Follow = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Follow = S::Follow; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `follow` field + pub struct follow(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct FollowAcceptBuilder<'a, S: follow_accept_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> FollowAccept<'a> { + /// Create a new builder for this type + pub fn new() -> FollowAcceptBuilder<'a, follow_accept_state::Empty> { + FollowAcceptBuilder::new() + } +} + +impl<'a> FollowAcceptBuilder<'a, follow_accept_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + FollowAcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FollowAcceptBuilder<'a, S> +where + S: follow_accept_state::State, + S::CreatedAt: follow_accept_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> FollowAcceptBuilder<'a, follow_accept_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + FollowAcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FollowAcceptBuilder<'a, S> +where + S: follow_accept_state::State, + S::Follow: follow_accept_state::IsUnset, +{ + /// Set the `follow` field (required) + pub fn follow( + mut self, + value: impl Into>, + ) -> FollowAcceptBuilder<'a, follow_accept_state::SetFollow> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + FollowAcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FollowAcceptBuilder<'a, S> +where + S: follow_accept_state::State, + S::Follow: follow_accept_state::IsSet, + S::CreatedAt: follow_accept_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> FollowAccept<'a> { + FollowAccept { + created_at: self.__unsafe_private_named.0.unwrap(), + follow: 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>, + >, + ) -> FollowAccept<'a> { + FollowAccept { + created_at: self.__unsafe_private_named.0.unwrap(), + follow: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> FollowAccept<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, FollowAcceptRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct FollowAcceptGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: FollowAccept<'a>, +} + +impl From> for FollowAccept<'_> { + fn from(output: FollowAcceptGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for FollowAccept<'_> { + const NSID: &'static str = "sh.weaver.graph.followAccept"; + type Record = FollowAcceptRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct FollowAcceptRecord; +impl jacquard_common::xrpc::XrpcResp for FollowAcceptRecord { + const NSID: &'static str = "sh.weaver.graph.followAccept"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = FollowAcceptGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for FollowAcceptRecord { + const NSID: &'static str = "sh.weaver.graph.followAccept"; + type Record = FollowAcceptRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for FollowAccept<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.followAccept" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_followAccept() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_followAccept() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.followAccept"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Acceptance of a follow request. Completes the two-way agreement.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("follow"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("follow"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", + ), + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/follow_gate.rs b/crates/weaver-api/src/sh_weaver/graph/follow_gate.rs new file mode 100644 index 0000000..36a8b8e --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/follow_gate.rs @@ -0,0 +1,322 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.followGate +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Settings controlling follow approval behavior. Absence means auto-accept. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct FollowGate<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// If true, previously auto-accepted follows are invalidated when requireApproval is enabled. Appview should treat followAccept records created before this gate's createdAt as invalid. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub invalidate_prior: std::option::Option, + /// If true, follows require manual acceptance. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub require_approval: std::option::Option, +} + +pub mod follow_gate_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; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type CreatedAt = 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; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct FollowGateBuilder<'a, S: follow_gate_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> FollowGate<'a> { + /// Create a new builder for this type + pub fn new() -> FollowGateBuilder<'a, follow_gate_state::Empty> { + FollowGateBuilder::new() + } +} + +impl<'a> FollowGateBuilder<'a, follow_gate_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + FollowGateBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FollowGateBuilder<'a, S> +where + S: follow_gate_state::State, + S::CreatedAt: follow_gate_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> FollowGateBuilder<'a, follow_gate_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + FollowGateBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: follow_gate_state::State> FollowGateBuilder<'a, S> { + /// Set the `invalidatePrior` field (optional) + pub fn invalidate_prior(mut self, value: impl Into>) -> Self { + self.__unsafe_private_named.1 = value.into(); + self + } + /// Set the `invalidatePrior` field to an Option value (optional) + pub fn maybe_invalidate_prior(mut self, value: Option) -> Self { + self.__unsafe_private_named.1 = value; + self + } +} + +impl<'a, S: follow_gate_state::State> FollowGateBuilder<'a, S> { + /// Set the `requireApproval` field (optional) + pub fn require_approval(mut self, value: impl Into>) -> Self { + self.__unsafe_private_named.2 = value.into(); + self + } + /// Set the `requireApproval` field to an Option value (optional) + pub fn maybe_require_approval(mut self, value: Option) -> Self { + self.__unsafe_private_named.2 = value; + self + } +} + +impl<'a, S> FollowGateBuilder<'a, S> +where + S: follow_gate_state::State, + S::CreatedAt: follow_gate_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> FollowGate<'a> { + FollowGate { + created_at: self.__unsafe_private_named.0.unwrap(), + invalidate_prior: self.__unsafe_private_named.1, + require_approval: self.__unsafe_private_named.2, + 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>, + >, + ) -> FollowGate<'a> { + FollowGate { + created_at: self.__unsafe_private_named.0.unwrap(), + invalidate_prior: self.__unsafe_private_named.1, + require_approval: self.__unsafe_private_named.2, + extra_data: Some(extra_data), + } + } +} + +impl<'a> FollowGate<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, FollowGateRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct FollowGateGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: FollowGate<'a>, +} + +impl From> for FollowGate<'_> { + fn from(output: FollowGateGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for FollowGate<'_> { + const NSID: &'static str = "sh.weaver.graph.followGate"; + type Record = FollowGateRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct FollowGateRecord; +impl jacquard_common::xrpc::XrpcResp for FollowGateRecord { + const NSID: &'static str = "sh.weaver.graph.followGate"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = FollowGateGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for FollowGateRecord { + const NSID: &'static str = "sh.weaver.graph.followGate"; + type Record = FollowGateRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for FollowGate<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.followGate" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_followGate() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_followGate() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.followGate"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Settings controlling follow approval behavior. Absence means auto-accept.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("self")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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( + "invalidatePrior", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { + description: None, + default: None, + r#const: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "requireApproval", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { + description: None, + default: None, + r#const: None, + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/like.rs b/crates/weaver-api/src/sh_weaver/graph/like.rs new file mode 100644 index 0000000..0b7c5e1 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/like.rs @@ -0,0 +1,312 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.like +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Record declaring a 'like' of a notebook or entry. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Like<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// The notebook or entry being liked. + #[serde(borrow)] + pub subject: crate::com_atproto::repo::strong_ref::StrongRef<'a>, +} + +pub mod like_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 Subject; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subject = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `subject` field to Set + pub struct SetSubject(PhantomData S>); + impl sealed::Sealed for SetSubject {} + impl State for SetSubject { + type Subject = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Subject = S::Subject; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subject` field + pub struct subject(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct LikeBuilder<'a, S: like_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> Like<'a> { + /// Create a new builder for this type + pub fn new() -> LikeBuilder<'a, like_state::Empty> { + LikeBuilder::new() + } +} + +impl<'a> LikeBuilder<'a, like_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + LikeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> LikeBuilder<'a, S> +where + S: like_state::State, + S::CreatedAt: like_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> LikeBuilder<'a, like_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + LikeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> LikeBuilder<'a, S> +where + S: like_state::State, + S::Subject: like_state::IsUnset, +{ + /// Set the `subject` field (required) + pub fn subject( + mut self, + value: impl Into>, + ) -> LikeBuilder<'a, like_state::SetSubject> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + LikeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> LikeBuilder<'a, S> +where + S: like_state::State, + S::Subject: like_state::IsSet, + S::CreatedAt: like_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Like<'a> { + Like { + created_at: self.__unsafe_private_named.0.unwrap(), + subject: 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>, + >, + ) -> Like<'a> { + Like { + created_at: self.__unsafe_private_named.0.unwrap(), + subject: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> Like<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, LikeRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct LikeGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: Like<'a>, +} + +impl From> for Like<'_> { + fn from(output: LikeGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for Like<'_> { + const NSID: &'static str = "sh.weaver.graph.like"; + type Record = LikeRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct LikeRecord; +impl jacquard_common::xrpc::XrpcResp for LikeRecord { + const NSID: &'static str = "sh.weaver.graph.like"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = LikeGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for LikeRecord { + const NSID: &'static str = "sh.weaver.graph.like"; + type Record = LikeRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Like<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.like" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_like() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_like() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.like"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Record declaring a 'like' of a notebook or entry.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("subject"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("subject"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", + ), + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/list.rs b/crates/weaver-api/src/sh_weaver/graph/list.rs new file mode 100644 index 0000000..037d118 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/list.rs @@ -0,0 +1,596 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.list +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ListPurpose<'a> { + ShWeaverGraphDefsCuratelist, + ShWeaverGraphDefsReadinglist, + Other(jacquard_common::CowStr<'a>), +} + +impl<'a> ListPurpose<'a> { + pub fn as_str(&self) -> &str { + match self { + Self::ShWeaverGraphDefsCuratelist => "sh.weaver.graph.defs#curatelist", + Self::ShWeaverGraphDefsReadinglist => "sh.weaver.graph.defs#readinglist", + Self::Other(s) => s.as_ref(), + } + } +} + +impl<'a> From<&'a str> for ListPurpose<'a> { + fn from(s: &'a str) -> Self { + match s { + "sh.weaver.graph.defs#curatelist" => Self::ShWeaverGraphDefsCuratelist, + "sh.weaver.graph.defs#readinglist" => Self::ShWeaverGraphDefsReadinglist, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> From for ListPurpose<'a> { + fn from(s: String) -> Self { + match s.as_str() { + "sh.weaver.graph.defs#curatelist" => Self::ShWeaverGraphDefsCuratelist, + "sh.weaver.graph.defs#readinglist" => Self::ShWeaverGraphDefsReadinglist, + _ => Self::Other(jacquard_common::CowStr::from(s)), + } + } +} + +impl<'a> AsRef for ListPurpose<'a> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl<'a> serde::Serialize for ListPurpose<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de, 'a> serde::Deserialize<'de> for ListPurpose<'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 ListPurpose<'_> { + type Output = ListPurpose<'static>; + fn into_static(self) -> Self::Output { + match self { + ListPurpose::ShWeaverGraphDefsCuratelist => { + ListPurpose::ShWeaverGraphDefsCuratelist + } + ListPurpose::ShWeaverGraphDefsReadinglist => { + ListPurpose::ShWeaverGraphDefsReadinglist + } + ListPurpose::Other(v) => ListPurpose::Other(v.into_static()), + } + } +} + +/// A curated list of notebooks and/or entries. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct List<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub avatar: std::option::Option>, + pub created_at: jacquard_common::types::string::Datetime, + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub description: std::option::Option>, + /// Display name for the list. + #[serde(borrow)] + pub name: jacquard_common::CowStr<'a>, + /// The purpose/type of list. + #[serde(borrow)] + pub purpose: crate::sh_weaver::graph::list::ListPurpose<'a>, +} + +pub mod list_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 Name; + type Purpose; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Name = Unset; + type Purpose = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `name` field to Set + pub struct SetName(PhantomData S>); + impl sealed::Sealed for SetName {} + impl State for SetName { + type Name = Set; + type Purpose = S::Purpose; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `purpose` field to Set + pub struct SetPurpose(PhantomData S>); + impl sealed::Sealed for SetPurpose {} + impl State for SetPurpose { + type Name = S::Name; + type Purpose = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Name = S::Name; + type Purpose = S::Purpose; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `name` field + pub struct name(()); + ///Marker type for the `purpose` field + pub struct purpose(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ListBuilder<'a, S: list_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>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> List<'a> { + /// Create a new builder for this type + pub fn new() -> ListBuilder<'a, list_state::Empty> { + ListBuilder::new() + } +} + +impl<'a> ListBuilder<'a, list_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ListBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: list_state::State> ListBuilder<'a, S> { + /// Set the `avatar` field (optional) + pub fn avatar( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.0 = value.into(); + self + } + /// Set the `avatar` field to an Option value (optional) + pub fn maybe_avatar( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.0 = value; + self + } +} + +impl<'a, S> ListBuilder<'a, S> +where + S: list_state::State, + S::CreatedAt: list_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> ListBuilder<'a, list_state::SetCreatedAt> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ListBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: list_state::State> ListBuilder<'a, S> { + /// Set the `description` field (optional) + pub fn description( + mut self, + value: impl Into>>, + ) -> Self { + self.__unsafe_private_named.2 = value.into(); + self + } + /// Set the `description` field to an Option value (optional) + pub fn maybe_description( + mut self, + value: Option>, + ) -> Self { + self.__unsafe_private_named.2 = value; + self + } +} + +impl<'a, S> ListBuilder<'a, S> +where + S: list_state::State, + S::Name: list_state::IsUnset, +{ + /// Set the `name` field (required) + pub fn name( + mut self, + value: impl Into>, + ) -> ListBuilder<'a, list_state::SetName> { + self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); + ListBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ListBuilder<'a, S> +where + S: list_state::State, + S::Purpose: list_state::IsUnset, +{ + /// Set the `purpose` field (required) + pub fn purpose( + mut self, + value: impl Into>, + ) -> ListBuilder<'a, list_state::SetPurpose> { + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); + ListBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ListBuilder<'a, S> +where + S: list_state::State, + S::Name: list_state::IsSet, + S::Purpose: list_state::IsSet, + S::CreatedAt: list_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> List<'a> { + List { + avatar: self.__unsafe_private_named.0, + created_at: self.__unsafe_private_named.1.unwrap(), + description: self.__unsafe_private_named.2, + name: self.__unsafe_private_named.3.unwrap(), + purpose: self.__unsafe_private_named.4.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>, + >, + ) -> List<'a> { + List { + avatar: self.__unsafe_private_named.0, + created_at: self.__unsafe_private_named.1.unwrap(), + description: self.__unsafe_private_named.2, + name: self.__unsafe_private_named.3.unwrap(), + purpose: self.__unsafe_private_named.4.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> List<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, ListRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ListGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: List<'a>, +} + +impl From> for List<'_> { + fn from(output: ListGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for List<'_> { + const NSID: &'static str = "sh.weaver.graph.list"; + type Record = ListRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ListRecord; +impl jacquard_common::xrpc::XrpcResp for ListRecord { + const NSID: &'static str = "sh.weaver.graph.list"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = ListGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for ListRecord { + const NSID: &'static str = "sh.weaver.graph.list"; + type Record = ListRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for List<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.list" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_list() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + if let Some(ref value) = self.description { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 3000usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "description", + ), + max: 3000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.description { + { + let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( + value.as_ref(), + true, + ) + .count(); + if count > 300usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxGraphemes { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "description", + ), + max: 300usize, + actual: count, + }); + } + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 64usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "name", + ), + max: 64usize, + actual: ::len(value.as_ref()), + }); + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) < 1usize { + return Err(::jacquard_lexicon::validation::ConstraintError::MinLength { + path: ::jacquard_lexicon::validation::ValidationPath::from_field( + "name", + ), + min: 1usize, + actual: ::len(value.as_ref()), + }); + } + } + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_list() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.list"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("listPurpose"), + ::jacquard_lexicon::lexicon::LexUserType::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "A curated list of notebooks and/or entries.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("name"), + ::jacquard_common::smol_str::SmolStr::new_static("purpose"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("avatar"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob { + description: None, + accept: None, + max_size: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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( + "description", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + format: None, + default: None, + min_length: None, + max_length: Some(3000usize), + min_graphemes: None, + max_graphemes: Some(300usize), + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("name"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Display name for the list.", + ), + ), + format: None, + default: None, + min_length: Some(1usize), + max_length: Some(64usize), + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("purpose"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static("#listPurpose"), + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/listitem.rs b/crates/weaver-api/src/sh_weaver/graph/listitem.rs new file mode 100644 index 0000000..114f355 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/listitem.rs @@ -0,0 +1,372 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.listitem +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// An item in a list. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Listitem<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// Reference to the list record. + #[serde(borrow)] + pub list: jacquard_common::types::string::AtUri<'a>, + /// The notebook or entry being added to the list. + #[serde(borrow)] + pub subject: crate::com_atproto::repo::strong_ref::StrongRef<'a>, +} + +pub mod listitem_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 Subject; + type List; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subject = Unset; + type List = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `subject` field to Set + pub struct SetSubject(PhantomData S>); + impl sealed::Sealed for SetSubject {} + impl State for SetSubject { + type Subject = Set; + type List = S::List; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `list` field to Set + pub struct SetList(PhantomData S>); + impl sealed::Sealed for SetList {} + impl State for SetList { + type Subject = S::Subject; + type List = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Subject = S::Subject; + type List = S::List; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subject` field + pub struct subject(()); + ///Marker type for the `list` field + pub struct list(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ListitemBuilder<'a, S: listitem_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> Listitem<'a> { + /// Create a new builder for this type + pub fn new() -> ListitemBuilder<'a, listitem_state::Empty> { + ListitemBuilder::new() + } +} + +impl<'a> ListitemBuilder<'a, listitem_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ListitemBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ListitemBuilder<'a, S> +where + S: listitem_state::State, + S::CreatedAt: listitem_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> ListitemBuilder<'a, listitem_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ListitemBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ListitemBuilder<'a, S> +where + S: listitem_state::State, + S::List: listitem_state::IsUnset, +{ + /// Set the `list` field (required) + pub fn list( + mut self, + value: impl Into>, + ) -> ListitemBuilder<'a, listitem_state::SetList> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ListitemBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ListitemBuilder<'a, S> +where + S: listitem_state::State, + S::Subject: listitem_state::IsUnset, +{ + /// Set the `subject` field (required) + pub fn subject( + mut self, + value: impl Into>, + ) -> ListitemBuilder<'a, listitem_state::SetSubject> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + ListitemBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ListitemBuilder<'a, S> +where + S: listitem_state::State, + S::Subject: listitem_state::IsSet, + S::List: listitem_state::IsSet, + S::CreatedAt: listitem_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Listitem<'a> { + Listitem { + created_at: self.__unsafe_private_named.0.unwrap(), + list: self.__unsafe_private_named.1.unwrap(), + subject: 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>, + >, + ) -> Listitem<'a> { + Listitem { + created_at: self.__unsafe_private_named.0.unwrap(), + list: self.__unsafe_private_named.1.unwrap(), + subject: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> Listitem<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, ListitemRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ListitemGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: Listitem<'a>, +} + +impl From> for Listitem<'_> { + fn from(output: ListitemGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for Listitem<'_> { + const NSID: &'static str = "sh.weaver.graph.listitem"; + type Record = ListitemRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ListitemRecord; +impl jacquard_common::xrpc::XrpcResp for ListitemRecord { + const NSID: &'static str = "sh.weaver.graph.listitem"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = ListitemGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for ListitemRecord { + const NSID: &'static str = "sh.weaver.graph.listitem"; + type Record = ListitemRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Listitem<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.listitem" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_listitem() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_listitem() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.listitem"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static("An item in a list."), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("subject"), + ::jacquard_common::smol_str::SmolStr::new_static("list"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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("list"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Reference to the list record.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::AtUri, + ), + 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("subject"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", + ), + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/subscribe.rs b/crates/weaver-api/src/sh_weaver/graph/subscribe.rs new file mode 100644 index 0000000..88e2b72 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/subscribe.rs @@ -0,0 +1,326 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.subscribe +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Request to subscribe to a notebook. Requires acceptance to be active. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Subscribe<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// URI of the notebook to subscribe to. + #[serde(borrow)] + pub notebook: jacquard_common::types::string::AtUri<'a>, +} + +pub mod subscribe_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 Notebook; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Notebook = Unset; + type CreatedAt = 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; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Notebook = S::Notebook; + type CreatedAt = 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 `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct SubscribeBuilder<'a, S: subscribe_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> Subscribe<'a> { + /// Create a new builder for this type + pub fn new() -> SubscribeBuilder<'a, subscribe_state::Empty> { + SubscribeBuilder::new() + } +} + +impl<'a> SubscribeBuilder<'a, subscribe_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + SubscribeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> SubscribeBuilder<'a, S> +where + S: subscribe_state::State, + S::CreatedAt: subscribe_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> SubscribeBuilder<'a, subscribe_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + SubscribeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> SubscribeBuilder<'a, S> +where + S: subscribe_state::State, + S::Notebook: subscribe_state::IsUnset, +{ + /// Set the `notebook` field (required) + pub fn notebook( + mut self, + value: impl Into>, + ) -> SubscribeBuilder<'a, subscribe_state::SetNotebook> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + SubscribeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> SubscribeBuilder<'a, S> +where + S: subscribe_state::State, + S::Notebook: subscribe_state::IsSet, + S::CreatedAt: subscribe_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Subscribe<'a> { + Subscribe { + created_at: self.__unsafe_private_named.0.unwrap(), + notebook: 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>, + >, + ) -> Subscribe<'a> { + Subscribe { + created_at: self.__unsafe_private_named.0.unwrap(), + notebook: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> Subscribe<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, SubscribeRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: Subscribe<'a>, +} + +impl From> for Subscribe<'_> { + fn from(output: SubscribeGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for Subscribe<'_> { + const NSID: &'static str = "sh.weaver.graph.subscribe"; + type Record = SubscribeRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SubscribeRecord; +impl jacquard_common::xrpc::XrpcResp for SubscribeRecord { + const NSID: &'static str = "sh.weaver.graph.subscribe"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = SubscribeGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for SubscribeRecord { + const NSID: &'static str = "sh.weaver.graph.subscribe"; + type Record = SubscribeRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Subscribe<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.subscribe" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_subscribe() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_subscribe() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.subscribe"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Request to subscribe to a notebook. Requires acceptance to be active.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::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("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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( + "notebook", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "URI of the notebook to subscribe to.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::AtUri, + ), + 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 + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/graph/subscribe_accept.rs b/crates/weaver-api/src/sh_weaver/graph/subscribe_accept.rs new file mode 100644 index 0000000..0062a2b --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/graph/subscribe_accept.rs @@ -0,0 +1,314 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.graph.subscribeAccept +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// Acceptance of a subscription request. +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeAccept<'a> { + pub created_at: jacquard_common::types::string::Datetime, + /// Reference to the subscribe record being accepted. + #[serde(borrow)] + pub subscribe: crate::com_atproto::repo::strong_ref::StrongRef<'a>, +} + +pub mod subscribe_accept_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 Subscribe; + type CreatedAt; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subscribe = Unset; + type CreatedAt = Unset; + } + ///State transition - sets the `subscribe` field to Set + pub struct SetSubscribe(PhantomData S>); + impl sealed::Sealed for SetSubscribe {} + impl State for SetSubscribe { + type Subscribe = Set; + type CreatedAt = S::CreatedAt; + } + ///State transition - sets the `created_at` field to Set + pub struct SetCreatedAt(PhantomData S>); + impl sealed::Sealed for SetCreatedAt {} + impl State for SetCreatedAt { + type Subscribe = S::Subscribe; + type CreatedAt = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subscribe` field + pub struct subscribe(()); + ///Marker type for the `created_at` field + pub struct created_at(()); + } +} + +/// Builder for constructing an instance of this type +pub struct SubscribeAcceptBuilder<'a, S: subscribe_accept_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> SubscribeAccept<'a> { + /// Create a new builder for this type + pub fn new() -> SubscribeAcceptBuilder<'a, subscribe_accept_state::Empty> { + SubscribeAcceptBuilder::new() + } +} + +impl<'a> SubscribeAcceptBuilder<'a, subscribe_accept_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + SubscribeAcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> SubscribeAcceptBuilder<'a, S> +where + S: subscribe_accept_state::State, + S::CreatedAt: subscribe_accept_state::IsUnset, +{ + /// Set the `createdAt` field (required) + pub fn created_at( + mut self, + value: impl Into, + ) -> SubscribeAcceptBuilder<'a, subscribe_accept_state::SetCreatedAt> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + SubscribeAcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> SubscribeAcceptBuilder<'a, S> +where + S: subscribe_accept_state::State, + S::Subscribe: subscribe_accept_state::IsUnset, +{ + /// Set the `subscribe` field (required) + pub fn subscribe( + mut self, + value: impl Into>, + ) -> SubscribeAcceptBuilder<'a, subscribe_accept_state::SetSubscribe> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + SubscribeAcceptBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> SubscribeAcceptBuilder<'a, S> +where + S: subscribe_accept_state::State, + S::Subscribe: subscribe_accept_state::IsSet, + S::CreatedAt: subscribe_accept_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> SubscribeAccept<'a> { + SubscribeAccept { + created_at: self.__unsafe_private_named.0.unwrap(), + subscribe: 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>, + >, + ) -> SubscribeAccept<'a> { + SubscribeAccept { + created_at: self.__unsafe_private_named.0.unwrap(), + subscribe: self.__unsafe_private_named.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> SubscribeAccept<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, SubscribeAcceptRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeAcceptGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: SubscribeAccept<'a>, +} + +impl From> for SubscribeAccept<'_> { + fn from(output: SubscribeAcceptGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for SubscribeAccept<'_> { + const NSID: &'static str = "sh.weaver.graph.subscribeAccept"; + type Record = SubscribeAcceptRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SubscribeAcceptRecord; +impl jacquard_common::xrpc::XrpcResp for SubscribeAcceptRecord { + const NSID: &'static str = "sh.weaver.graph.subscribeAccept"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = SubscribeAcceptGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for SubscribeAcceptRecord { + const NSID: &'static str = "sh.weaver.graph.subscribeAccept"; + type Record = SubscribeAcceptRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SubscribeAccept<'a> { + fn nsid() -> &'static str { + "sh.weaver.graph.subscribeAccept" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_graph_subscribeAccept() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +fn lexicon_doc_sh_weaver_graph_subscribeAccept() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.subscribeAccept"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "Acceptance of a subscription request.", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("subscribe"), + ::jacquard_common::smol_str::SmolStr::new_static("createdAt") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "createdAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + 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( + "subscribe", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", + ), + }), + ); + map + }, + }), + }), + ); + map + }, + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/notebook.rs b/crates/weaver-api/src/sh_weaver/notebook.rs index ee8a414..416f640 100644 --- a/crates/weaver-api/src/sh_weaver/notebook.rs +++ b/crates/weaver-api/src/sh_weaver/notebook.rs @@ -10,6 +10,10 @@ pub mod book; pub mod chapter; pub mod colour_scheme; pub mod entry; +pub mod get_entry; +pub mod get_entry_by_title; +pub mod get_notebook; +pub mod get_notebook_by_title; pub mod page; pub mod theme; diff --git a/crates/weaver-api/src/sh_weaver/notebook/book.rs b/crates/weaver-api/src/sh_weaver/notebook/book.rs index 3c836da..43ffe38 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/book.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/book.rs @@ -37,6 +37,9 @@ pub struct Book<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] pub title: std::option::Option>, + /// Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub updated_at: std::option::Option, } pub mod book_state { @@ -94,6 +97,7 @@ pub struct BookBuilder<'a, S: book_state::State> { ::core::option::Option>, ::core::option::Option>, ::core::option::Option>, + ::core::option::Option, ), _phantom: ::core::marker::PhantomData<&'a ()>, } @@ -110,7 +114,7 @@ impl<'a> BookBuilder<'a, book_state::Empty> { pub fn new() -> Self { BookBuilder { _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), _phantom: ::core::marker::PhantomData, } } @@ -249,6 +253,25 @@ impl<'a, S: book_state::State> BookBuilder<'a, S> { } } +impl<'a, S: book_state::State> BookBuilder<'a, S> { + /// Set the `updatedAt` field (optional) + pub fn updated_at( + mut self, + value: impl Into>, + ) -> Self { + self.__unsafe_private_named.7 = value.into(); + self + } + /// Set the `updatedAt` field to an Option value (optional) + pub fn maybe_updated_at( + mut self, + value: Option, + ) -> Self { + self.__unsafe_private_named.7 = value; + self + } +} + impl<'a, S> BookBuilder<'a, S> where S: book_state::State, @@ -265,6 +288,7 @@ where tags: self.__unsafe_private_named.4, theme: self.__unsafe_private_named.5, title: self.__unsafe_private_named.6, + updated_at: self.__unsafe_private_named.7, extra_data: Default::default(), } } @@ -284,6 +308,7 @@ where tags: self.__unsafe_private_named.4, theme: self.__unsafe_private_named.5, title: self.__unsafe_private_named.6, + updated_at: self.__unsafe_private_named.7, extra_data: Some(extra_data), } } @@ -487,6 +512,29 @@ fn lexicon_doc_sh_weaver_notebook_book() -> ::jacquard_lexicon::lexicon::Lexicon ), }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "updatedAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios.", + ), + ), + 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 }, }), diff --git a/crates/weaver-api/src/sh_weaver/notebook/entry.rs b/crates/weaver-api/src/sh_weaver/notebook/entry.rs index a286d87..b572f0b 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/entry.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/entry.rs @@ -34,6 +34,9 @@ pub struct Entry<'a> { pub tags: std::option::Option>, #[serde(borrow)] pub title: crate::sh_weaver::notebook::Title<'a>, + /// Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub updated_at: std::option::Option, } pub mod entry_state { @@ -120,6 +123,7 @@ pub struct EntryBuilder<'a, S: entry_state::State> { ::core::option::Option>, ::core::option::Option>, ::core::option::Option>, + ::core::option::Option, ), _phantom: ::core::marker::PhantomData<&'a ()>, } @@ -136,7 +140,7 @@ impl<'a> EntryBuilder<'a, entry_state::Empty> { pub fn new() -> Self { EntryBuilder { _phantom_state: ::core::marker::PhantomData, - __unsafe_private_named: (None, None, None, None, None, None), + __unsafe_private_named: (None, None, None, None, None, None, None), _phantom: ::core::marker::PhantomData, } } @@ -250,6 +254,25 @@ where } } +impl<'a, S: entry_state::State> EntryBuilder<'a, S> { + /// Set the `updatedAt` field (optional) + pub fn updated_at( + mut self, + value: impl Into>, + ) -> Self { + self.__unsafe_private_named.6 = value.into(); + self + } + /// Set the `updatedAt` field to an Option value (optional) + pub fn maybe_updated_at( + mut self, + value: Option, + ) -> Self { + self.__unsafe_private_named.6 = value; + self + } +} + impl<'a, S> EntryBuilder<'a, S> where S: entry_state::State, @@ -267,6 +290,7 @@ where path: self.__unsafe_private_named.3.unwrap(), tags: self.__unsafe_private_named.4, title: self.__unsafe_private_named.5.unwrap(), + updated_at: self.__unsafe_private_named.6, extra_data: Default::default(), } } @@ -285,6 +309,7 @@ where path: self.__unsafe_private_named.3.unwrap(), tags: self.__unsafe_private_named.4, title: self.__unsafe_private_named.5.unwrap(), + updated_at: self.__unsafe_private_named.6, extra_data: Some(extra_data), } } @@ -507,6 +532,29 @@ fn lexicon_doc_sh_weaver_notebook_entry() -> ::jacquard_lexicon::lexicon::Lexico ), }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "updatedAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios.", + ), + ), + 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 }, }), diff --git a/crates/weaver-api/src/sh_weaver/notebook/get_entry.rs b/crates/weaver-api/src/sh_weaver/notebook/get_entry.rs new file mode 100644 index 0000000..3b90a3f --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/notebook/get_entry.rs @@ -0,0 +1,216 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.notebook.getEntry +// +// 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 GetEntry<'a> { + ///(default: 0, min: 0) + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub index: std::option::Option, + #[serde(borrow)] + pub notebook: jacquard_common::types::string::AtUri<'a>, +} + +pub mod get_entry_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 Notebook; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Notebook = 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; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `notebook` field + pub struct notebook(()); + } +} + +/// Builder for constructing an instance of this type +pub struct GetEntryBuilder<'a, S: get_entry_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> GetEntry<'a> { + /// Create a new builder for this type + pub fn new() -> GetEntryBuilder<'a, get_entry_state::Empty> { + GetEntryBuilder::new() + } +} + +impl<'a> GetEntryBuilder<'a, get_entry_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + GetEntryBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S: get_entry_state::State> GetEntryBuilder<'a, S> { + /// Set the `index` field (optional) + pub fn index(mut self, value: impl Into>) -> Self { + self.__unsafe_private_named.0 = value.into(); + self + } + /// Set the `index` field to an Option value (optional) + pub fn maybe_index(mut self, value: Option) -> Self { + self.__unsafe_private_named.0 = value; + self + } +} + +impl<'a, S> GetEntryBuilder<'a, S> +where + S: get_entry_state::State, + S::Notebook: get_entry_state::IsUnset, +{ + /// Set the `notebook` field (required) + pub fn notebook( + mut self, + value: impl Into>, + ) -> GetEntryBuilder<'a, get_entry_state::SetNotebook> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + GetEntryBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetEntryBuilder<'a, S> +where + S: get_entry_state::State, + S::Notebook: get_entry_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> GetEntry<'a> { + GetEntry { + index: self.__unsafe_private_named.0, + notebook: self.__unsafe_private_named.1.unwrap(), + } + } +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct GetEntryOutput<'a> { + #[serde(flatten)] + #[serde(borrow)] + pub value: crate::sh_weaver::notebook::BookEntryView<'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 GetEntryError<'a> { + #[serde(rename = "NotebookNotFound")] + NotebookNotFound(std::option::Option), + #[serde(rename = "EntryNotFound")] + EntryNotFound(std::option::Option), +} + +impl std::fmt::Display for GetEntryError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotebookNotFound(msg) => { + write!(f, "NotebookNotFound")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::EntryNotFound(msg) => { + write!(f, "EntryNotFound")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), + } + } +} + +/// Response type for +///sh.weaver.notebook.getEntry +pub struct GetEntryResponse; +impl jacquard_common::xrpc::XrpcResp for GetEntryResponse { + const NSID: &'static str = "sh.weaver.notebook.getEntry"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = GetEntryOutput<'de>; + type Err<'de> = GetEntryError<'de>; +} + +impl<'a> jacquard_common::xrpc::XrpcRequest for GetEntry<'a> { + const NSID: &'static str = "sh.weaver.notebook.getEntry"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = GetEntryResponse; +} + +/// Endpoint type for +///sh.weaver.notebook.getEntry +pub struct GetEntryRequest; +impl jacquard_common::xrpc::XrpcEndpoint for GetEntryRequest { + const PATH: &'static str = "/xrpc/sh.weaver.notebook.getEntry"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = GetEntry<'de>; + type Response = GetEntryResponse; +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/notebook/get_entry_by_title.rs b/crates/weaver-api/src/sh_weaver/notebook/get_entry_by_title.rs new file mode 100644 index 0000000..d0bb77c --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/notebook/get_entry_by_title.rs @@ -0,0 +1,236 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.notebook.getEntryByTitle +// +// 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 GetEntryByTitle<'a> { + #[serde(borrow)] + pub notebook: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub title: jacquard_common::CowStr<'a>, +} + +pub mod get_entry_by_title_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 Notebook; + type Title; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Notebook = Unset; + type Title = 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; + type Title = S::Title; + } + ///State transition - sets the `title` field to Set + pub struct SetTitle(PhantomData S>); + impl sealed::Sealed for SetTitle {} + impl State for SetTitle { + type Notebook = S::Notebook; + type Title = 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 `title` field + pub struct title(()); + } +} + +/// Builder for constructing an instance of this type +pub struct GetEntryByTitleBuilder<'a, S: get_entry_by_title_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> GetEntryByTitle<'a> { + /// Create a new builder for this type + pub fn new() -> GetEntryByTitleBuilder<'a, get_entry_by_title_state::Empty> { + GetEntryByTitleBuilder::new() + } +} + +impl<'a> GetEntryByTitleBuilder<'a, get_entry_by_title_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + GetEntryByTitleBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetEntryByTitleBuilder<'a, S> +where + S: get_entry_by_title_state::State, + S::Notebook: get_entry_by_title_state::IsUnset, +{ + /// Set the `notebook` field (required) + pub fn notebook( + mut self, + value: impl Into>, + ) -> GetEntryByTitleBuilder<'a, get_entry_by_title_state::SetNotebook> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + GetEntryByTitleBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetEntryByTitleBuilder<'a, S> +where + S: get_entry_by_title_state::State, + S::Title: get_entry_by_title_state::IsUnset, +{ + /// Set the `title` field (required) + pub fn title( + mut self, + value: impl Into>, + ) -> GetEntryByTitleBuilder<'a, get_entry_by_title_state::SetTitle> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + GetEntryByTitleBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetEntryByTitleBuilder<'a, S> +where + S: get_entry_by_title_state::State, + S::Notebook: get_entry_by_title_state::IsSet, + S::Title: get_entry_by_title_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> GetEntryByTitle<'a> { + GetEntryByTitle { + notebook: self.__unsafe_private_named.0.unwrap(), + title: self.__unsafe_private_named.1.unwrap(), + } + } +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct GetEntryByTitleOutput<'a> { + #[serde(borrow)] + pub entry: crate::sh_weaver::notebook::BookEntryView<'a>, + /// The raw entry record data. + #[serde(borrow)] + pub record: jacquard_common::types::value::Data<'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 GetEntryByTitleError<'a> { + #[serde(rename = "NotebookNotFound")] + NotebookNotFound(std::option::Option), + #[serde(rename = "EntryNotFound")] + EntryNotFound(std::option::Option), +} + +impl std::fmt::Display for GetEntryByTitleError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotebookNotFound(msg) => { + write!(f, "NotebookNotFound")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::EntryNotFound(msg) => { + write!(f, "EntryNotFound")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), + } + } +} + +/// Response type for +///sh.weaver.notebook.getEntryByTitle +pub struct GetEntryByTitleResponse; +impl jacquard_common::xrpc::XrpcResp for GetEntryByTitleResponse { + const NSID: &'static str = "sh.weaver.notebook.getEntryByTitle"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = GetEntryByTitleOutput<'de>; + type Err<'de> = GetEntryByTitleError<'de>; +} + +impl<'a> jacquard_common::xrpc::XrpcRequest for GetEntryByTitle<'a> { + const NSID: &'static str = "sh.weaver.notebook.getEntryByTitle"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = GetEntryByTitleResponse; +} + +/// Endpoint type for +///sh.weaver.notebook.getEntryByTitle +pub struct GetEntryByTitleRequest; +impl jacquard_common::xrpc::XrpcEndpoint for GetEntryByTitleRequest { + const PATH: &'static str = "/xrpc/sh.weaver.notebook.getEntryByTitle"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = GetEntryByTitle<'de>; + type Response = GetEntryByTitleResponse; +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/notebook/get_notebook.rs b/crates/weaver-api/src/sh_weaver/notebook/get_notebook.rs new file mode 100644 index 0000000..234f2d9 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/notebook/get_notebook.rs @@ -0,0 +1,156 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.notebook.getNotebook +// +// 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 GetNotebook<'a> { + #[serde(borrow)] + pub notebook: jacquard_common::types::string::AtUri<'a>, +} + +pub mod get_notebook_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 Notebook; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Notebook = 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; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `notebook` field + pub struct notebook(()); + } +} + +/// Builder for constructing an instance of this type +pub struct GetNotebookBuilder<'a, S: get_notebook_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> GetNotebook<'a> { + /// Create a new builder for this type + pub fn new() -> GetNotebookBuilder<'a, get_notebook_state::Empty> { + GetNotebookBuilder::new() + } +} + +impl<'a> GetNotebookBuilder<'a, get_notebook_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + GetNotebookBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None,), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetNotebookBuilder<'a, S> +where + S: get_notebook_state::State, + S::Notebook: get_notebook_state::IsUnset, +{ + /// Set the `notebook` field (required) + pub fn notebook( + mut self, + value: impl Into>, + ) -> GetNotebookBuilder<'a, get_notebook_state::SetNotebook> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + GetNotebookBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetNotebookBuilder<'a, S> +where + S: get_notebook_state::State, + S::Notebook: get_notebook_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> GetNotebook<'a> { + GetNotebook { + notebook: self.__unsafe_private_named.0.unwrap(), + } + } +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct GetNotebookOutput<'a> { + #[serde(borrow)] + pub entries: Vec>, + #[serde(borrow)] + pub notebook: crate::sh_weaver::notebook::NotebookView<'a>, +} + +/// Response type for +///sh.weaver.notebook.getNotebook +pub struct GetNotebookResponse; +impl jacquard_common::xrpc::XrpcResp for GetNotebookResponse { + const NSID: &'static str = "sh.weaver.notebook.getNotebook"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = GetNotebookOutput<'de>; + type Err<'de> = jacquard_common::xrpc::GenericError<'de>; +} + +impl<'a> jacquard_common::xrpc::XrpcRequest for GetNotebook<'a> { + const NSID: &'static str = "sh.weaver.notebook.getNotebook"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = GetNotebookResponse; +} + +/// Endpoint type for +///sh.weaver.notebook.getNotebook +pub struct GetNotebookRequest; +impl jacquard_common::xrpc::XrpcEndpoint for GetNotebookRequest { + const PATH: &'static str = "/xrpc/sh.weaver.notebook.getNotebook"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = GetNotebook<'de>; + type Response = GetNotebookResponse; +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/notebook/get_notebook_by_title.rs b/crates/weaver-api/src/sh_weaver/notebook/get_notebook_by_title.rs new file mode 100644 index 0000000..28de921 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/notebook/get_notebook_by_title.rs @@ -0,0 +1,226 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.notebook.getNotebookByTitle +// +// 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 GetNotebookByTitle<'a> { + #[serde(borrow)] + pub actor: jacquard_common::types::ident::AtIdentifier<'a>, + #[serde(borrow)] + pub title: jacquard_common::CowStr<'a>, +} + +pub mod get_notebook_by_title_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 Actor; + type Title; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Actor = Unset; + type Title = Unset; + } + ///State transition - sets the `actor` field to Set + pub struct SetActor(PhantomData S>); + impl sealed::Sealed for SetActor {} + impl State for SetActor { + type Actor = Set; + type Title = S::Title; + } + ///State transition - sets the `title` field to Set + pub struct SetTitle(PhantomData S>); + impl sealed::Sealed for SetTitle {} + impl State for SetTitle { + type Actor = S::Actor; + type Title = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `actor` field + pub struct actor(()); + ///Marker type for the `title` field + pub struct title(()); + } +} + +/// Builder for constructing an instance of this type +pub struct GetNotebookByTitleBuilder<'a, S: get_notebook_by_title_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> GetNotebookByTitle<'a> { + /// Create a new builder for this type + pub fn new() -> GetNotebookByTitleBuilder<'a, get_notebook_by_title_state::Empty> { + GetNotebookByTitleBuilder::new() + } +} + +impl<'a> GetNotebookByTitleBuilder<'a, get_notebook_by_title_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + GetNotebookByTitleBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetNotebookByTitleBuilder<'a, S> +where + S: get_notebook_by_title_state::State, + S::Actor: get_notebook_by_title_state::IsUnset, +{ + /// Set the `actor` field (required) + pub fn actor( + mut self, + value: impl Into>, + ) -> GetNotebookByTitleBuilder<'a, get_notebook_by_title_state::SetActor> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + GetNotebookByTitleBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetNotebookByTitleBuilder<'a, S> +where + S: get_notebook_by_title_state::State, + S::Title: get_notebook_by_title_state::IsUnset, +{ + /// Set the `title` field (required) + pub fn title( + mut self, + value: impl Into>, + ) -> GetNotebookByTitleBuilder<'a, get_notebook_by_title_state::SetTitle> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + GetNotebookByTitleBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> GetNotebookByTitleBuilder<'a, S> +where + S: get_notebook_by_title_state::State, + S::Actor: get_notebook_by_title_state::IsSet, + S::Title: get_notebook_by_title_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> GetNotebookByTitle<'a> { + GetNotebookByTitle { + actor: self.__unsafe_private_named.0.unwrap(), + title: self.__unsafe_private_named.1.unwrap(), + } + } +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct GetNotebookByTitleOutput<'a> { + #[serde(borrow)] + pub entries: Vec>, + #[serde(borrow)] + pub notebook: crate::sh_weaver::notebook::NotebookView<'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 GetNotebookByTitleError<'a> { + #[serde(rename = "NotebookNotFound")] + NotebookNotFound(std::option::Option), +} + +impl std::fmt::Display for GetNotebookByTitleError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotebookNotFound(msg) => { + write!(f, "NotebookNotFound")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), + } + } +} + +/// Response type for +///sh.weaver.notebook.getNotebookByTitle +pub struct GetNotebookByTitleResponse; +impl jacquard_common::xrpc::XrpcResp for GetNotebookByTitleResponse { + const NSID: &'static str = "sh.weaver.notebook.getNotebookByTitle"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = GetNotebookByTitleOutput<'de>; + type Err<'de> = GetNotebookByTitleError<'de>; +} + +impl<'a> jacquard_common::xrpc::XrpcRequest for GetNotebookByTitle<'a> { + const NSID: &'static str = "sh.weaver.notebook.getNotebookByTitle"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = GetNotebookByTitleResponse; +} + +/// Endpoint type for +///sh.weaver.notebook.getNotebookByTitle +pub struct GetNotebookByTitleRequest; +impl jacquard_common::xrpc::XrpcEndpoint for GetNotebookByTitleRequest { + const PATH: &'static str = "/xrpc/sh.weaver.notebook.getNotebookByTitle"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = GetNotebookByTitle<'de>; + type Response = GetNotebookByTitleResponse; +} \ No newline at end of file diff --git a/crates/weaver-app/.env-example b/crates/weaver-app/.env-dev similarity index 100% rename from crates/weaver-app/.env-example rename to crates/weaver-app/.env-dev diff --git a/crates/weaver-app/assets/styling/drafts.css b/crates/weaver-app/assets/styling/drafts.css new file mode 100644 index 0000000..c4adc8d --- /dev/null +++ b/crates/weaver-app/assets/styling/drafts.css @@ -0,0 +1,102 @@ +.drafts-page { + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} + +.drafts-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.drafts-header h1 { + margin: 0; +} + +.drafts-empty { + text-align: center; + padding: 4rem 2rem; + color: var(--color-subtle); +} + +.drafts-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.draft-card { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 0; + transition: border-color 0.15s ease; +} + +.draft-card:hover { + border-color: var(--color-primary); +} + +.draft-card-link { + flex: 1; + text-decoration: none; + color: inherit; +} + +.draft-card-content { + display: flex; + align-items: center; + gap: 1rem; +} + +.draft-title { + margin: 0; + font-size: 1rem; + font-weight: 500; +} + +.draft-badge { + font-size: 0.75rem; + padding: 0.125rem 0.5rem; + border-radius: 4px; + font-weight: 500; +} + +.draft-badge-new { + background: var(--color-secondary); + color: var(--color-base); +} + +.draft-badge-edit { + background: var(--color-surface); + color: var(--color-subtle); + border: 1px solid var(--color-border); +} + +/* Mobile adjustments */ +@media (max-width: 600px) { + .drafts-page { + padding: 1rem; + } + + .drafts-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .draft-card { + padding: 0.75rem; + } + + .draft-card-content { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } +} diff --git a/crates/weaver-app/assets/styling/entry-actions.css b/crates/weaver-app/assets/styling/entry-actions.css new file mode 100644 index 0000000..c9d4965 --- /dev/null +++ b/crates/weaver-app/assets/styling/entry-actions.css @@ -0,0 +1,91 @@ +.entry-actions { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.entry-action-link { + text-decoration: none; +} + +/* Match notebook-add-entry styling for buttons */ +.entry-actions .button { + font-size: 0.85rem; + color: var(--color-primary); + background: transparent; + padding: 0.25rem 0.5rem; + border-radius: 0; + border: 1px solid var(--color-border); + transition: + background-color 0.15s ease, + color 0.15s ease; +} + +.entry-actions .button:hover { + background-color: var(--color-primary); + color: var(--color-base); +} + +.entry-actions-dropdown { + position: relative; +} + +.dropdown-menu { + position: absolute; + right: 0; + top: 100%; + background: var(--color-surface); + border: 1px solid var(--color-border); + padding: 0.25rem; + z-index: 100; + min-width: 160px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +@media (prefers-color-scheme: dark) { + .dropdown-menu { + box-shadow: none; + border: 1px dashed var(--color-border); + } +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + text-align: left; + background: none; + border: none; + cursor: pointer; + font-size: 0.875rem; + color: var(--color-text); +} + +.dropdown-item:hover { + background: var(--color-highlight); +} + +.dropdown-item-danger { + color: var(--color-error); +} + +.dropdown-item-danger:hover { + background: var(--color-error); + color: var(--color-base); +} + +.dialog-error { + color: var(--destructive); + font-size: 0.875rem; + padding: 0.5rem; + background: var(--destructive-muted, rgba(220, 38, 38, 0.1)); + border-radius: 0; + margin-bottom: 1rem; +} + +.dialog-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + margin-top: 1rem; +} diff --git a/crates/weaver-app/assets/styling/entry-card.css b/crates/weaver-app/assets/styling/entry-card.css index 3e9bfac..497133a 100644 --- a/crates/weaver-app/assets/styling/entry-card.css +++ b/crates/weaver-app/assets/styling/entry-card.css @@ -79,10 +79,23 @@ } .entry-card-header { + display: flex; + align-items: baseline; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.entry-card-title-link { + flex: 1; + text-decoration: none; + color: inherit; +} + +.entry-card-title-link:hover .entry-card-title { + color: var(--color-secondary); } .entry-card-title { - margin-left: auto; font-size: 1.25rem; font-weight: 600; color: var(--color-primary); @@ -92,8 +105,6 @@ } .entry-card-meta { - display: flex; - align-items: center; gap: 1rem; margin-bottom: 0.5rem; font-size: 0.875rem; diff --git a/crates/weaver-app/assets/styling/entry.css b/crates/weaver-app/assets/styling/entry.css index dbfe5f8..55bca4c 100644 --- a/crates/weaver-app/assets/styling/entry.css +++ b/crates/weaver-app/assets/styling/entry.css @@ -105,10 +105,18 @@ border-bottom: 2px solid var(--color-border); } +.entry-header-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; +} + .entry-title { margin-bottom: calc(1rem * var(--spacing-scale, 1.5)); margin-top: calc(1rem * var(--spacing-scale, 1.5)); color: var(--color-text); + flex: 1; } .entry-meta-info { diff --git a/crates/weaver-app/assets/styling/notebook-card.css b/crates/weaver-app/assets/styling/notebook-card.css index 5a98507..46132f5 100644 --- a/crates/weaver-app/assets/styling/notebook-card.css +++ b/crates/weaver-app/assets/styling/notebook-card.css @@ -87,6 +87,35 @@ border-bottom: 2px solid var(--color-border); } +.notebook-card-header-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} + +.notebook-add-entry { + font-size: 0.85rem; + color: var(--color-primary); + text-decoration: none; + padding: 0.25rem 0.5rem; + border: 1px solid var(--color-border); + border-radius: 0; + transition: + background-color 0.15s ease, + color 0.15s ease; + white-space: nowrap; +} + +.notebook-add-entry:hover { + background-color: var(--color-primary); + color: var(--color-base); +} + +.notebook-actions { + margin-bottom: 1.5rem; +} + .notebook-card-title { font-size: 1.5rem; font-weight: 600; @@ -152,12 +181,27 @@ .entry-preview-header { display: flex; - justify-content: space-between; align-items: baseline; gap: 1rem; margin-bottom: 0.5rem; } +.entry-preview-title-link { + flex: 1; + text-decoration: none; + color: inherit; +} + +.entry-preview-title-link:hover .entry-preview-title { + color: var(--color-secondary); +} + +.entry-preview-content-link { + display: block; + text-decoration: none; + color: inherit; +} + .entry-preview-title { color: var(--color-text); font-weight: 600; diff --git a/crates/weaver-app/assets/styling/notebook-cover.css b/crates/weaver-app/assets/styling/notebook-cover.css index b01c042..f2ad67a 100644 --- a/crates/weaver-app/assets/styling/notebook-cover.css +++ b/crates/weaver-app/assets/styling/notebook-cover.css @@ -108,3 +108,22 @@ color: var(--color-subtle); border-bottom: 1px solid var(--color-border); } + +/* Owner actions */ +.notebook-cover-actions { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border); + border-radius: 0; +} + +.notebook-cover-action-link { + text-decoration: none; + border-radius: 0; +} + +.notebook-cover-actions .button { + width: 100%; + text-align: left; + border-radius: 0; +} diff --git a/crates/weaver-app/assets/styling/profile-actions.css b/crates/weaver-app/assets/styling/profile-actions.css new file mode 100644 index 0000000..4e8799a --- /dev/null +++ b/crates/weaver-app/assets/styling/profile-actions.css @@ -0,0 +1,82 @@ +.profile-actions { + grid-column: 3; + position: sticky; + top: 2rem; + align-self: flex-start; +} + +.profile-actions-container { + padding-top: 2.25rem; + padding-right: 5rem; +} + +.profile-actions-title { + font-size: 0.75rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--color-subtle); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.profile-actions-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.profile-action-link { + display: block; + text-decoration: none; +} + +/* Match notebook-add-entry styling */ +.profile-actions-list .button { + width: 100%; + text-align: left; + font-size: 0.85rem; + color: var(--color-primary); + background: transparent; + padding: 0.25rem 0.5rem; + border-radius: 0; + border: 1px solid var(--color-border); + transition: + background-color 0.15s ease, + color 0.15s ease; +} + +.profile-actions-list .button:hover { + background-color: var(--color-primary); + color: var(--color-base); +} + +.profile-actions-list .button:disabled { + opacity: 0.5; + cursor: not-allowed; + border-radius: 0; +} + +.profile-actions-list .button:disabled:hover { + background: transparent; + color: var(--color-primary); +} + +/* Mobile menubar */ +.profile-actions-menubar { + display: none; + gap: 0.5rem; + padding: 1rem 0; + border-bottom: 1px solid var(--color-border); + margin-bottom: 1rem; +} + +/* Hide sidebar on mobile, show menubar */ +@media (max-width: 1400px) { + .profile-actions { + display: none; + } + + .profile-actions-menubar { + display: flex; + } +} diff --git a/crates/weaver-app/src/components/dialog/component.rs b/crates/weaver-app/src/components/dialog/component.rs index 1cb4bff..4c7cde2 100644 --- a/crates/weaver-app/src/components/dialog/component.rs +++ b/crates/weaver-app/src/components/dialog/component.rs @@ -3,19 +3,41 @@ use dioxus_primitives::dialog::{ self, DialogContentProps, DialogDescriptionProps, DialogRootProps, DialogTitleProps, }; +const DIALOG_CSS: Asset = asset!("./dialog.css"); + +/// Fixed-position overlay that works in both SSR and client-only modes. +/// Wraps dioxus_primitives DialogRoot with inline positioning styles +/// since the CSS file doesn't load reliably in client-only mode. #[component] pub fn DialogRoot(props: DialogRootProps) -> Element { + let is_open = props.open.read().unwrap_or(false); + + let overlay_style = if is_open { + "position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center;" + } else { + "display: none;" + }; + rsx! { - document::Link { rel: "stylesheet", href: asset!("./dialog.css") } - dialog::DialogRoot { - class: "dialog-backdrop", - id: props.id, - is_modal: props.is_modal, - open: props.open, - default_open: props.default_open, - on_open_change: props.on_open_change, - attributes: props.attributes, - {props.children} + document::Link { rel: "stylesheet", href: DIALOG_CSS } + div { + style: "{overlay_style}", + onclick: { + let on_change = props.on_open_change.clone(); + move |_| { + on_change.call(false); + } + }, + dialog::DialogRoot { + class: "dialog-backdrop-inner", + id: props.id, + is_modal: props.is_modal, + open: props.open, + default_open: props.default_open, + on_open_change: props.on_open_change, + attributes: props.attributes, + {props.children} + } } } } @@ -23,7 +45,15 @@ pub fn DialogRoot(props: DialogRootProps) -> Element { #[component] pub fn DialogContent(props: DialogContentProps) -> Element { rsx! { - dialog::DialogContent { class: "dialog", id: props.id, attributes: props.attributes, {props.children} } + div { + onclick: move |e| e.stop_propagation(), + dialog::DialogContent { + class: "dialog", + id: props.id, + attributes: props.attributes, + {props.children} + } + } } } diff --git a/crates/weaver-app/src/components/editor/component.rs b/crates/weaver-app/src/components/editor/component.rs index 8f049e8..642ab5d 100644 --- a/crates/weaver-app/src/components/editor/component.rs +++ b/crates/weaver-app/src/components/editor/component.rs @@ -4,6 +4,7 @@ use dioxus::prelude::*; use jacquard::IntoStatic; use jacquard::cowstr::ToCowStr; use jacquard::identity::resolver::IdentityResolver; +use jacquard::smol_str::SmolStr; use jacquard::types::blob::BlobRef; use jacquard::types::ident::AtIdentifier; use weaver_api::sh_weaver::embed::images::Image; @@ -14,10 +15,10 @@ use crate::components::editor::ReportButton; use crate::fetch::Fetcher; use super::actions::{ - execute_action, handle_keydown_with_bindings, EditorAction, Key, KeyCombo, KeybindingConfig, - KeydownResult, Range, + EditorAction, Key, KeyCombo, KeybindingConfig, KeydownResult, Range, execute_action, + handle_keydown_with_bindings, }; -use super::beforeinput::{handle_beforeinput, BeforeInputContext, BeforeInputResult, InputType}; +use super::beforeinput::{BeforeInputContext, BeforeInputResult, InputType, handle_beforeinput}; #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] use super::beforeinput::{get_data_from_event, get_target_range_from_event}; use super::document::{CompositionState, EditorDocument, LoadedDocState}; @@ -57,20 +58,30 @@ enum LoadResult { /// # Props /// - `initial_content`: Optional initial markdown content (for new entries) /// - `entry_uri`: Optional AT-URI of an existing entry to edit +/// - `target_notebook`: Optional notebook title to add the entry to when publishing #[component] -pub fn MarkdownEditor(initial_content: Option, entry_uri: Option) -> Element { +pub fn MarkdownEditor( + initial_content: Option, + entry_uri: Option, + target_notebook: Option, +) -> Element { let fetcher = use_context::(); // Determine draft key - use entry URI if editing existing, otherwise generate TID let draft_key = use_hook(|| { entry_uri.clone().unwrap_or_else(|| { - format!("new:{}", jacquard::types::tid::Ticker::new().next(None).as_str()) + format!( + "new:{}", + jacquard::types::tid::Ticker::new().next(None).as_str() + ) }) }); // Parse entry URI once let parsed_uri = entry_uri.as_ref().and_then(|s| { - jacquard::types::string::AtUri::new(s).ok().map(|u| u.into_static()) + jacquard::types::string::AtUri::new(s) + .ok() + .map(|u| u.into_static()) }); // Clone draft_key for render (resource closure moves it) @@ -112,7 +123,7 @@ pub fn MarkdownEditor(initial_content: Option, entry_uri: Option entry_authority ); return LoadResult::Failed( - "You can only edit your own entries".to_string() + "You can only edit your own entries".to_string(), ); } } @@ -215,10 +226,7 @@ pub fn MarkdownEditor(initial_content: Option, entry_uri: Option /// - PDS sync with auto-save /// - Keyboard shortcuts (Ctrl+B for bold, Ctrl+I for italic) #[component] -fn MarkdownEditorInner( - draft_key: String, - loaded_state: LoadedDocState, -) -> Element { +fn MarkdownEditorInner(draft_key: String, loaded_state: LoadedDocState) -> Element { // Context for authenticated API calls let fetcher = use_context::(); let auth_state = use_context::>(); @@ -353,6 +361,10 @@ fn MarkdownEditorInner( // Track last saved frontiers to detect changes (peek-only, no subscriptions) let mut last_saved_frontiers: Signal> = use_signal(|| None); + // Store interval handle so it's dropped when component unmounts (prevents panic) + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + let mut interval_holder: Signal> = use_signal(|| None); + // Auto-save with periodic check (no reactive dependency to avoid loops) #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] let doc_for_autosave = document.clone(); @@ -383,7 +395,8 @@ fn MarkdownEditorInner( last_saved_frontiers.set(Some(current_frontiers)); } }); - interval.forget(); + // Store in signal instead of forget - interval drops when component unmounts + interval_holder.set(Some(interval)); }); // Set up beforeinput listener for all text input handling. @@ -466,7 +479,9 @@ fn MarkdownEditorInner( if let Some(window) = web_sys::window() { if let Some(doc) = window.document() { if let Some(elem) = doc.get_element_by_id(editor_id) { - if let Some(html_elem) = elem.dyn_ref::() { + if let Some(html_elem) = + elem.dyn_ref::() + { let _ = html_elem.blur(); let _ = html_elem.focus(); } @@ -817,14 +832,14 @@ fn MarkdownEditorInner( if plat.android && evt.key() == Key::Enter { tracing::debug!("Android: handling Enter in keypress"); evt.prevent_default(); - + // Get current range let range = if let Some(sel) = *doc.selection.read() { Range::new(sel.anchor.min(sel.head), sel.anchor.max(sel.head)) } else { Range::caret(doc.cursor.read().offset) }; - + let action = EditorAction::InsertParagraph { range }; execute_action(&mut doc, &action); } diff --git a/crates/weaver-app/src/components/entry.rs b/crates/weaver-app/src/components/entry.rs index 61f9626..4d1f2ee 100644 --- a/crates/weaver-app/src/components/entry.rs +++ b/crates/weaver-app/src/components/entry.rs @@ -1,14 +1,16 @@ #![allow(non_snake_case)] +use crate::Route; #[cfg(feature = "server")] use crate::blobcache::BlobCache; use crate::{ + components::EntryActions, components::avatar::{Avatar, AvatarImage}, data::use_handle, }; - -use crate::Route; use dioxus::prelude::*; +use jacquard::IntoStatic; +use jacquard::types::aturi::AtUri; const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); @@ -66,29 +68,31 @@ pub fn EntryPage( })); // Handle blob caching when entry data is available - match &*entry.read() { + match &*entry.read_unchecked() { Some((book_entry_view, entry_record)) => { if let Some(embeds) = &entry_record.embeds { if let Some(_images) = &embeds.images { // Register blob mappings with service worker (client-side only) - #[cfg(all( - target_family = "wasm", - target_os = "unknown", - not(feature = "fullstack-server") - ))] - { - let fetcher = fetcher.clone(); - let images = images.clone(); - spawn(async move { - let _ = crate::service_worker::register_entry_blobs( - &ident(), - book_title().as_str(), - &_images, - &fetcher, - ) - .await; - }); - } + // #[cfg(all( + // target_family = "wasm", + // target_os = "unknown", + // not(feature = "fullstack-server") + // ))] + // { + // let fetcher = fetcher.clone(); + // let images = _images.clone().into_static(); + // spawn(async move { + // let images = images.clone(); + // let fetcher = fetcher.clone(); + // let _ = crate::service_worker::register_entry_blobs( + // &ident(), + // book_title().as_str(), + // &_images, + // &fetcher, + // ) + // .await; + // }); + // } } } rsx! { EntryPageView { @@ -143,7 +147,10 @@ fn EntryPageView( // Metadata header EntryMetadata { entry_view: entry_view.clone(), - created_at: entry_record().created_at.clone() + created_at: entry_record().created_at.clone(), + entry_uri: entry_view.uri.clone().into_static(), + book_title: Some(book_title()), + ident: ident() } // Rendered markdown @@ -176,9 +183,19 @@ pub fn EntryCard( ident: AtIdentifier<'static>, ) -> Element { use crate::Route; + use crate::auth::AuthState; use jacquard::from_data; use weaver_api::sh_weaver::notebook::entry::Entry; + let mut hidden = use_signal(|| false); + + // If removed from notebook, hide this card + if hidden() { + return rsx! {}; + } + + let auth_state = use_context::>(); + let entry_view = &entry.entry; let title = entry_view .title @@ -192,6 +209,17 @@ pub fn EntryCard( .format("%B %d, %Y") .to_string(); + // Check ownership + let is_owner = { + let current_did = auth_state.read().did.clone(); + match (¤t_did, &ident) { + (Some(did), AtIdentifier::Did(ident_did)) => *did == *ident_did, + _ => false, + } + }; + + let entry_uri = entry_view.uri.clone().into_static(); + // Only show author if notebook has multiple authors let show_author = author_count > 1; let first_author = if show_author { @@ -210,85 +238,86 @@ pub fn EntryCard( rsx! { div { class: "entry-card", - Link { - to: Route::EntryPage { - ident: ident, - book_title: book_title.clone(), - title: title.to_string().into() - }, - class: "entry-card-link", - - - - div { class: "entry-card-meta", - div { class: "entry-card-header", - + div { class: "entry-card-meta", + div { class: "entry-card-header", + Link { + to: Route::EntryPage { + ident: ident.clone(), + book_title: book_title.clone(), + title: title.to_string().into() + }, + class: "entry-card-title-link", h3 { class: "entry-card-title", "{title}" } - div { class: "entry-card-date", - time { datetime: "{entry_view.indexed_at.as_str()}", "{formatted_date}" } + } + div { class: "entry-card-date", + time { datetime: "{entry_view.indexed_at.as_str()}", "{formatted_date}" } + } + if is_owner { + EntryActions { + entry_uri, + entry_title: title.to_string(), + in_notebook: true, + notebook_title: Some(book_title.clone()), + on_removed: Some(EventHandler::new(move |_| hidden.set(true))) } } - if let Some(author) = first_author { - div { class: "entry-card-author", - { - use weaver_api::sh_weaver::actor::ProfileDataViewInner; - - match &author.record.inner { - ProfileDataViewInner::ProfileView(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { - if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { src: avatar_url.as_ref() } - } + } + if let Some(author) = first_author { + div { class: "entry-card-author", + { + use weaver_api::sh_weaver::actor::ProfileDataViewInner; + + match &author.record.inner { + ProfileDataViewInner::ProfileView(profile) => { + let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); + let handle = profile.handle.clone(); + rsx! { + if let Some(ref avatar_url) = profile.avatar { + Avatar { + AvatarImage { src: avatar_url.as_ref() } } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } } + span { class: "author-name", "{display_name}" } + span { class: "meta-label", "@{handle}" } } - ProfileDataViewInner::ProfileViewDetailed(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { - if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { src: avatar_url.as_ref() } - } + } + ProfileDataViewInner::ProfileViewDetailed(profile) => { + let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); + let handle = profile.handle.clone(); + rsx! { + if let Some(ref avatar_url) = profile.avatar { + Avatar { + AvatarImage { src: avatar_url.as_ref() } } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } } + span { class: "author-name", "{display_name}" } + span { class: "meta-label", "@{handle}" } } - ProfileDataViewInner::TangledProfileView(profile) => { - rsx! { - span { class: "author-name", "@{profile.handle.as_ref()}" } - } + } + ProfileDataViewInner::TangledProfileView(profile) => { + rsx! { + span { class: "author-name", "@{profile.handle.as_ref()}" } } - _ => { - rsx! { - span { class: "author-name", "Unknown" } - } + } + _ => { + rsx! { + span { class: "author-name", "Unknown" } } } } } } - - } + } - - - if let Some(ref html) = preview_html { - div { class: "entry-card-preview", dangerous_inner_html: "{html}" } - } - if let Some(ref tags) = entry_view.tags { - if !tags.is_empty() { - div { class: "entry-card-tags", - for tag in tags.iter() { - span { class: "entry-card-tag", "{tag}" } - } + if let Some(ref html) = preview_html { + div { class: "entry-card-preview", dangerous_inner_html: "{html}" } + } + if let Some(ref tags) = entry_view.tags { + if !tags.is_empty() { + div { class: "entry-card-tags", + for tag in tags.iter() { + span { class: "entry-card-tag", "{tag}" } } } } @@ -299,18 +328,47 @@ pub fn EntryCard( /// Metadata header showing title, authors, date, tags #[component] -fn EntryMetadata(entry_view: EntryView<'static>, created_at: Datetime) -> Element { +fn EntryMetadata( + entry_view: EntryView<'static>, + created_at: Datetime, + entry_uri: AtUri<'static>, + book_title: Option, + ident: AtIdentifier<'static>, +) -> Element { + let navigator = use_navigator(); + let title = entry_view .title .as_ref() .map(|t| t.as_ref()) .unwrap_or("Untitled"); - //let indexed_at_chrono = entry_view.indexed_at.as_ref(); + let entry_title = title.to_string(); + + // Navigate back to notebook when entry is removed + let nav_book_title = book_title.clone(); + let nav_ident = ident.clone(); + let on_removed = move |_| { + if let Some(ref title) = nav_book_title { + navigator.push(Route::NotebookIndex { + ident: nav_ident.clone(), + book_title: title.clone(), + }); + } + }; rsx! { header { class: "entry-metadata", - h1 { class: "entry-title", "{title}" } + div { class: "entry-header-row", + h1 { class: "entry-title", "{title}" } + EntryActions { + entry_uri: entry_uri.clone(), + entry_title, + in_notebook: book_title.is_some(), + notebook_title: book_title.clone(), + on_removed: Some(EventHandler::new(on_removed)) + } + } div { class: "entry-meta-info", // Authors diff --git a/crates/weaver-app/src/components/entry_actions.rs b/crates/weaver-app/src/components/entry_actions.rs new file mode 100644 index 0000000..4310694 --- /dev/null +++ b/crates/weaver-app/src/components/entry_actions.rs @@ -0,0 +1,371 @@ +//! Action buttons for entries (edit, delete, remove from notebook). + +use crate::Route; +use crate::auth::AuthState; +use crate::components::button::{Button, ButtonVariant}; +use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; +use crate::fetch::Fetcher; +use dioxus::prelude::*; +use jacquard::smol_str::SmolStr; +use jacquard::types::aturi::AtUri; +use jacquard::types::ident::AtIdentifier; +use jacquard::IntoStatic; +use weaver_api::com_atproto::repo::delete_record::DeleteRecord; +use weaver_api::com_atproto::repo::put_record::PutRecord; + +const ENTRY_ACTIONS_CSS: Asset = asset!("/assets/styling/entry-actions.css"); + +#[derive(Props, Clone, PartialEq)] +pub struct EntryActionsProps { + /// The AT-URI of the entry + pub entry_uri: AtUri<'static>, + /// The entry title (for display in confirmation) + pub entry_title: String, + /// Whether this entry is in a notebook (enables "remove from notebook") + #[props(default = false)] + pub in_notebook: bool, + /// Notebook title (if in_notebook is true, used for edit route) + #[props(default)] + pub notebook_title: Option, + /// Callback when entry is removed from notebook (for optimistic UI update) + #[props(default)] + pub on_removed: Option>, +} + +/// Action buttons for an entry: edit, delete, optionally remove from notebook. +#[component] +pub fn EntryActions(props: EntryActionsProps) -> Element { + let auth_state = use_context::>(); + let fetcher = use_context::(); + let navigator = use_navigator(); + + let mut show_delete_confirm = use_signal(|| false); + let mut show_remove_confirm = use_signal(|| false); + let mut show_dropdown = use_signal(|| false); + let mut deleting = use_signal(|| false); + let mut removing = use_signal(|| false); + let mut error = use_signal(|| None::); + + // Check ownership - compare auth DID with entry's authority + let current_did = auth_state.read().did.clone(); + let entry_authority = props.entry_uri.authority(); + let is_owner = match (¤t_did, entry_authority) { + (Some(current), AtIdentifier::Did(entry_did)) => *current == *entry_did, + _ => false, + }; + + if !is_owner { + return rsx! {}; + } + + // Extract rkey from URI for edit route + let rkey = match props.entry_uri.rkey() { + Some(r) => r.0.to_string(), + None => return rsx! {}, // Can't edit without rkey + }; + + // Build edit route based on whether entry is in a notebook + let ident = props.entry_uri.authority().clone(); + let edit_route = if props.in_notebook { + if let Some(ref notebook) = props.notebook_title { + Route::NotebookEntryEdit { + ident: ident.into_static(), + book_title: notebook.clone(), + rkey: rkey.clone().into(), + } + } else { + Route::StandaloneEntryEdit { + ident: ident.into_static(), + rkey: rkey.clone().into(), + } + } + } else { + Route::StandaloneEntryEdit { + ident: ident.into_static(), + rkey: rkey.clone().into(), + } + }; + + let entry_uri_for_delete = props.entry_uri.clone(); + let entry_title = props.entry_title.clone(); + + let delete_fetcher = fetcher.clone(); + let handle_delete = move |_| { + let fetcher = delete_fetcher.clone(); + let uri = entry_uri_for_delete.clone(); + let navigator = navigator.clone(); + + spawn(async move { + use jacquard::prelude::*; + + deleting.set(true); + error.set(None); + + let client = fetcher.get_client(); + let collection = uri.collection(); + let rkey = uri.rkey(); + + if let (Some(collection), Some(rkey)) = (collection, rkey) { + let did = match fetcher.current_did().await { + Some(d) => d, + None => { + error.set(Some("Not authenticated".to_string())); + deleting.set(false); + return; + } + }; + + let request = DeleteRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(collection.clone()) + .rkey(rkey.clone()) + .build(); + + match client.send(request).await { + Ok(_) => { + show_delete_confirm.set(false); + // Navigate back to home after delete + navigator.push(Route::Home {}); + } + Err(e) => { + error.set(Some(format!("Delete failed: {:?}", e))); + } + } + } else { + error.set(Some("Invalid entry URI".to_string())); + } + deleting.set(false); + }); + }; + + // Handler for removing entry from notebook (keeps entry, just removes from notebook's list) + let entry_uri_for_remove = props.entry_uri.clone(); + let notebook_title_for_remove = props.notebook_title.clone(); + let on_removed = props.on_removed.clone(); + let handle_remove_from_notebook = move |_| { + let fetcher = fetcher.clone(); + let entry_uri = entry_uri_for_remove.clone(); + let notebook_title = notebook_title_for_remove.clone(); + let on_removed = on_removed.clone(); + + spawn(async move { + use jacquard::{from_data, to_data, prelude::*, types::string::Nsid}; + use weaver_api::sh_weaver::notebook::book::Book; + + let client = fetcher.get_client(); + + removing.set(true); + error.set(None); + + let notebook_title = match notebook_title { + Some(t) => t, + None => { + error.set(Some("No notebook specified".to_string())); + removing.set(false); + return; + } + }; + + let did = match fetcher.current_did().await { + Some(d) => d, + None => { + error.set(Some("Not authenticated".to_string())); + removing.set(false); + return; + } + }; + + // Get the notebook by title + let ident = AtIdentifier::Did(did.clone()); + let notebook_result = fetcher.get_notebook(ident.clone(), notebook_title.clone()).await; + + let (notebook_view, _) = match notebook_result { + Ok(Some(data)) => data.as_ref().clone(), + Ok(None) => { + error.set(Some("Notebook not found".to_string())); + removing.set(false); + return; + } + Err(e) => { + error.set(Some(format!("Failed to get notebook: {:?}", e))); + removing.set(false); + return; + } + }; + + // Parse the book record to get the entry_list + let mut book: Book = match from_data(¬ebook_view.record) { + Ok(b) => b, + Err(e) => { + error.set(Some(format!("Failed to parse notebook: {:?}", e))); + removing.set(false); + return; + } + }; + + // Filter out the entry + let entry_uri_str = entry_uri.as_str(); + let original_len = book.entry_list.len(); + book.entry_list.retain(|ref_| ref_.uri.as_str() != entry_uri_str); + + if book.entry_list.len() == original_len { + error.set(Some("Entry not found in notebook".to_string())); + removing.set(false); + return; + } + + // Get the notebook's rkey from its URI + let notebook_rkey = match notebook_view.uri.rkey() { + Some(r) => r, + None => { + error.set(Some("Invalid notebook URI".to_string())); + removing.set(false); + return; + } + }; + + // Convert book to Data for the request + let book_data = match to_data(&book) { + Ok(d) => d, + Err(e) => { + error.set(Some(format!("Failed to serialize notebook: {:?}", e))); + removing.set(false); + return; + } + }; + + // Update the notebook record + let request = PutRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(Nsid::new_static("sh.weaver.notebook.book").unwrap()) + .rkey(notebook_rkey.clone()) + .record(book_data) + .build(); + + match client.send(request).await { + Ok(_) => { + show_remove_confirm.set(false); + // Notify parent to remove from local state + if let Some(handler) = &on_removed { + handler.call(()); + } + } + Err(e) => { + error.set(Some(format!("Failed to update notebook: {:?}", e))); + } + } + removing.set(false); + }); + }; + + rsx! { + document::Link { rel: "stylesheet", href: ENTRY_ACTIONS_CSS } + + div { class: "entry-actions", + // Edit button (always visible for owner) + Link { + to: edit_route, + class: "entry-action-link", + Button { + variant: ButtonVariant::Ghost, + "Edit" + } + } + + // Dropdown for destructive actions + div { class: "entry-actions-dropdown", + Button { + variant: ButtonVariant::Ghost, + onclick: move |_| show_dropdown.toggle(), + "⋮" + } + + if show_dropdown() { + div { class: "dropdown-menu", + if props.in_notebook { + button { + class: "dropdown-item", + onclick: move |_| { + show_dropdown.set(false); + show_remove_confirm.set(true); + }, + "Remove from notebook" + } + } + button { + class: "dropdown-item dropdown-item-danger", + onclick: move |_| { + show_dropdown.set(false); + show_delete_confirm.set(true); + }, + "Delete" + } + } + } + } + + // Delete confirmation dialog + DialogRoot { + open: show_delete_confirm(), + on_open_change: move |open: bool| show_delete_confirm.set(open), + DialogContent { + DialogTitle { "Delete Entry?" } + DialogDescription { + "Delete \"{entry_title}\"? This removes the published entry. You can restore from drafts if needed." + } + if let Some(ref err) = error() { + div { class: "dialog-error", "{err}" } + } + div { class: "dialog-actions", + Button { + variant: ButtonVariant::Destructive, + onclick: handle_delete, + disabled: deleting(), + if deleting() { "Deleting..." } else { "Delete" } + } + Button { + variant: ButtonVariant::Ghost, + onclick: move |_| show_delete_confirm.set(false), + "Cancel" + } + } + } + } + + // Remove from notebook confirmation dialog + if props.in_notebook { + { + let entry_title_for_remove = entry_title.clone(); + rsx! { + DialogRoot { + open: show_remove_confirm(), + on_open_change: move |open: bool| show_remove_confirm.set(open), + DialogContent { + DialogTitle { "Remove from Notebook?" } + DialogDescription { + "Remove \"{entry_title_for_remove}\" from this notebook? The entry will still exist but won't be part of this notebook." + } + if let Some(ref err) = error() { + div { class: "dialog-error", "{err}" } + } + div { class: "dialog-actions", + Button { + variant: ButtonVariant::Primary, + onclick: handle_remove_from_notebook, + disabled: removing(), + if removing() { "Removing..." } else { "Remove" } + } + Button { + variant: ButtonVariant::Ghost, + onclick: move |_| show_remove_confirm.set(false), + "Cancel" + } + } + } + } + } + } + } + } + } +} diff --git a/crates/weaver-app/src/components/identity.rs b/crates/weaver-app/src/components/identity.rs index dee4db9..3178f8c 100644 --- a/crates/weaver-app/src/components/identity.rs +++ b/crates/weaver-app/src/components/identity.rs @@ -1,3 +1,5 @@ +use crate::auth::AuthState; +use crate::components::{ProfileActions, ProfileActionsMenubar}; use crate::{Route, data, fetch}; use dioxus::prelude::*; use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier}; @@ -47,6 +49,9 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { // Main content area main { class: "repository-main", + // Mobile menubar (hidden on desktop) + ProfileActionsMenubar { ident } + div { class: "notebooks-list", match &*notebooks.read() { Some(notebook_list) => rsx! { @@ -72,6 +77,9 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { } } } + + // Actions sidebar (desktop only) + ProfileActions { ident } } } } @@ -84,6 +92,7 @@ pub fn NotebookCard( use jacquard::IntoStatic; let fetcher = use_context::(); + let auth_state = use_context::>(); let title = notebook .title @@ -91,6 +100,16 @@ pub fn NotebookCard( .map(|t| t.as_ref()) .unwrap_or("Untitled Notebook"); + // Check ownership for "Add Entry" link + let notebook_ident = notebook.uri.authority().clone().into_static(); + let is_owner = { + let current_did = auth_state.read().did.clone(); + match (¤t_did, ¬ebook_ident) { + (Some(did), AtIdentifier::Did(nb_did)) => *did == *nb_did, + _ => false, + } + }; + // Format date let formatted_date = notebook.indexed_at.as_ref().format("%B %d, %Y").to_string(); @@ -126,7 +145,16 @@ pub fn NotebookCard( class: "notebook-card-header-link", div { class: "notebook-card-header", - h2 { class: "notebook-card-title", "{title}" } + div { class: "notebook-card-header-top", + h2 { class: "notebook-card-title", "{title}" } + if is_owner { + Link { + to: Route::NewDraft { ident: notebook_ident.clone(), notebook: Some(book_title.clone()) }, + class: "notebook-add-entry", + "+ Add" + } + } + } div { class: "notebook-card-date", time { datetime: "{notebook.indexed_at.as_str()}", "{formatted_date}" } @@ -199,23 +227,40 @@ pub fn NotebookCard( let created_at = from_data::(&entry_view.entry.record).ok() .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string()); + let entry_uri = entry_view.entry.uri.clone().into_static(); + rsx! { - Link { - to: Route::EntryPage { - ident: ident.clone(), - book_title: book_title.clone(), - title: entry_title.to_string().into() - }, - class: "notebook-entry-preview-link", - - div { class: "notebook-entry-preview", - div { class: "entry-preview-header", + div { class: "notebook-entry-preview", + div { class: "entry-preview-header", + Link { + to: Route::EntryPage { + ident: ident.clone(), + book_title: book_title.clone(), + title: entry_title.to_string().into() + }, + class: "entry-preview-title-link", div { class: "entry-preview-title", "{entry_title}" } - if let Some(ref date) = created_at { - div { class: "entry-preview-date", "{date}" } + } + if let Some(ref date) = created_at { + div { class: "entry-preview-date", "{date}" } + } + if is_owner { + crate::components::EntryActions { + entry_uri, + entry_title: entry_title.to_string(), + in_notebook: true, + notebook_title: Some(book_title.clone()) } } - if let Some(ref html) = preview_html { + } + if let Some(ref html) = preview_html { + Link { + to: Route::EntryPage { + ident: ident.clone(), + book_title: book_title.clone(), + title: entry_title.to_string().into() + }, + class: "entry-preview-content-link", div { class: "entry-preview-content", dangerous_inner_html: "{html}" } } } @@ -243,23 +288,40 @@ pub fn NotebookCard( let created_at = from_data::(&first_entry.entry.record).ok() .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string()); + let entry_uri = first_entry.entry.uri.clone().into_static(); + rsx! { - Link { - to: Route::EntryPage { - ident: ident.clone(), - book_title: book_title.clone(), - title: entry_title.to_string().into() - }, - class: "notebook-entry-preview-link", - - div { class: "notebook-entry-preview notebook-entry-preview-first", - div { class: "entry-preview-header", + div { class: "notebook-entry-preview notebook-entry-preview-first", + div { class: "entry-preview-header", + Link { + to: Route::EntryPage { + ident: ident.clone(), + book_title: book_title.clone(), + title: entry_title.to_string().into() + }, + class: "entry-preview-title-link", div { class: "entry-preview-title", "{entry_title}" } - if let Some(ref date) = created_at { - div { class: "entry-preview-date", "{date}" } + } + if let Some(ref date) = created_at { + div { class: "entry-preview-date", "{date}" } + } + if is_owner { + crate::components::EntryActions { + entry_uri, + entry_title: entry_title.to_string(), + in_notebook: true, + notebook_title: Some(book_title.clone()) } } - if let Some(ref html) = preview_html { + } + if let Some(ref html) = preview_html { + Link { + to: Route::EntryPage { + ident: ident.clone(), + book_title: book_title.clone(), + title: entry_title.to_string().into() + }, + class: "entry-preview-content-link", div { class: "entry-preview-content", dangerous_inner_html: "{html}" } } } @@ -296,23 +358,40 @@ pub fn NotebookCard( let created_at = from_data::(&last_entry.entry.record).ok() .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string()); + let entry_uri = last_entry.entry.uri.clone().into_static(); + rsx! { - Link { - to: Route::EntryPage { - ident: ident.clone(), - book_title: book_title.clone(), - title: entry_title.to_string().into() - }, - class: "notebook-entry-preview-link", - - div { class: "notebook-entry-preview notebook-entry-preview-last", - div { class: "entry-preview-header", + div { class: "notebook-entry-preview notebook-entry-preview-last", + div { class: "entry-preview-header", + Link { + to: Route::EntryPage { + ident: ident.clone(), + book_title: book_title.clone(), + title: entry_title.to_string().into() + }, + class: "entry-preview-title-link", div { class: "entry-preview-title", "{entry_title}" } - if let Some(ref date) = created_at { - div { class: "entry-preview-date", "{date}" } + } + if let Some(ref date) = created_at { + div { class: "entry-preview-date", "{date}" } + } + if is_owner { + crate::components::EntryActions { + entry_uri, + entry_title: entry_title.to_string(), + in_notebook: true, + notebook_title: Some(book_title.clone()) } } - if let Some(ref html) = preview_html { + } + if let Some(ref html) = preview_html { + Link { + to: Route::EntryPage { + ident: ident.clone(), + book_title: book_title.clone(), + title: entry_title.to_string().into() + }, + class: "entry-preview-content-link", div { class: "entry-preview-content", dangerous_inner_html: "{html}" } } } diff --git a/crates/weaver-app/src/components/mod.rs b/crates/weaver-app/src/components/mod.rs index b5f877e..fcbce2c 100644 --- a/crates/weaver-app/src/components/mod.rs +++ b/crates/weaver-app/src/components/mod.rs @@ -128,4 +128,9 @@ pub mod accordion; pub mod button; pub mod dialog; pub mod editor; +pub mod entry_actions; pub mod input; +pub mod profile_actions; + +pub use entry_actions::EntryActions; +pub use profile_actions::{ProfileActions, ProfileActionsMenubar}; diff --git a/crates/weaver-app/src/components/notebook_cover.rs b/crates/weaver-app/src/components/notebook_cover.rs index e7495c9..aa24842 100644 --- a/crates/weaver-app/src/components/notebook_cover.rs +++ b/crates/weaver-app/src/components/notebook_cover.rs @@ -1,13 +1,22 @@ #![allow(non_snake_case)] +use crate::Route; use crate::components::avatar::{Avatar, AvatarImage}; +use crate::components::button::{Button, ButtonVariant}; use dioxus::prelude::*; +use jacquard::smol_str::SmolStr; +use jacquard::types::ident::AtIdentifier; use weaver_api::sh_weaver::notebook::NotebookView; const NOTEBOOK_COVER_CSS: Asset = asset!("/assets/styling/notebook-cover.css"); #[component] -pub fn NotebookCover(notebook: NotebookView<'static>, title: String) -> Element { +pub fn NotebookCover( + notebook: NotebookView<'static>, + title: String, + #[props(default = false)] is_owner: bool, + #[props(default)] ident: Option>, +) -> Element { use jacquard::from_data; use weaver_api::sh_weaver::notebook::book::Book; @@ -69,6 +78,25 @@ pub fn NotebookCover(notebook: NotebookView<'static>, title: String) -> Element } } } + + // Owner actions + if is_owner { + if let Some(ref owner_ident) = ident { + div { class: "notebook-cover-actions", + Link { + to: Route::NewDraft { + ident: owner_ident.clone(), + notebook: Some(SmolStr::from(title.as_str())) + }, + class: "notebook-cover-action-link", + Button { + variant: ButtonVariant::Outline, + "+ Add Entry" + } + } + } + } + } } } } diff --git a/crates/weaver-app/src/components/profile_actions.rs b/crates/weaver-app/src/components/profile_actions.rs new file mode 100644 index 0000000..98126b9 --- /dev/null +++ b/crates/weaver-app/src/components/profile_actions.rs @@ -0,0 +1,101 @@ +//! Actions sidebar/menubar for profile page. + +use crate::Route; +use crate::auth::AuthState; +use crate::components::button::{Button, ButtonVariant}; +use dioxus::prelude::*; +use jacquard::types::ident::AtIdentifier; + +const PROFILE_ACTIONS_CSS: Asset = asset!("/assets/styling/profile-actions.css"); + +/// Actions available on the profile page for the owner. +#[component] +pub fn ProfileActions(ident: ReadSignal>) -> Element { + let auth_state = use_context::>(); + + // Check if viewing own profile + let is_owner = { + let current_did = auth_state.read().did.clone(); + match (¤t_did, ident()) { + (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did, + _ => false, + } + }; + + if !is_owner { + return rsx! {}; + } + + rsx! { + document::Link { rel: "stylesheet", href: PROFILE_ACTIONS_CSS } + + aside { class: "profile-actions", + div { class: "profile-actions-container", + div { class: "profile-actions-list", + Link { + to: Route::NewDraft { ident: ident(), notebook: None }, + class: "profile-action-link", + Button { + variant: ButtonVariant::Outline, + "New Entry" + } + } + + // TODO: New Notebook button (disabled for now) + Button { + variant: ButtonVariant::Outline, + disabled: true, + "New Notebook" + } + + Link { + to: Route::DraftsList { ident: ident() }, + class: "profile-action-link", + Button { + variant: ButtonVariant::Ghost, + "Drafts" + } + } + } + } + } + } +} + +/// Mobile-friendly menubar version of profile actions. +#[component] +pub fn ProfileActionsMenubar(ident: ReadSignal>) -> Element { + let auth_state = use_context::>(); + + let is_owner = { + let current_did = auth_state.read().did.clone(); + match (¤t_did, ident()) { + (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did, + _ => false, + } + }; + + if !is_owner { + return rsx! {}; + } + + rsx! { + div { class: "profile-actions-menubar", + Link { + to: Route::NewDraft { ident: ident(), notebook: None }, + Button { + variant: ButtonVariant::Primary, + "New Entry" + } + } + + Link { + to: Route::DraftsList { ident: ident() }, + Button { + variant: ButtonVariant::Ghost, + "Drafts" + } + } + } + } +} diff --git a/crates/weaver-app/src/data.rs b/crates/weaver-app/src/data.rs index 95f2377..b50f80f 100644 --- a/crates/weaver-app/src/data.rs +++ b/crates/weaver-app/src/data.rs @@ -518,7 +518,7 @@ pub fn use_profile_data( None } }); - (res, memo); + (res, memo) } /// Fetches notebooks for a specific DID @@ -644,7 +644,7 @@ pub fn use_notebooks_from_ufos() -> ( /// Fetches notebooks from UFOS client-side only (no SSR) #[cfg(not(feature = "fullstack-server"))] pub fn use_notebooks_from_ufos() -> ( - Resource, Vec>)>>>, + Resource>>, Memo, Vec>)>>>, ) { let fetcher = use_context::(); @@ -810,6 +810,88 @@ pub fn use_notebook_entries( (r, memo) } +// ============================================================================ +// Ownership Checking +// ============================================================================ + +/// Check if the current authenticated user owns a resource identified by an AtIdentifier. +/// +/// Returns a memo that is: +/// - `Some(true)` if the user is authenticated and their DID matches the resource owner +/// - `Some(false)` if the user is authenticated but doesn't match, or resource is a handle +/// - `None` if the user is not authenticated +/// +/// For handles, this does a synchronous check that returns `false` since we can't resolve +/// handles synchronously. Use `use_is_owner_async` for handle resolution. +pub fn use_is_owner(resource_owner: ReadSignal>) -> Memo> { + let auth_state = use_context::>(); + + use_memo(move || { + let current_did = auth_state.read().did.clone()?; + let owner = resource_owner(); + + match owner { + AtIdentifier::Did(did) => Some(did == current_did), + AtIdentifier::Handle(_) => Some(false), // Can't resolve synchronously + } + }) +} + +/// Check ownership with async handle resolution. +/// +/// Returns a resource that resolves to: +/// - `Some(true)` if the user owns the resource +/// - `Some(false)` if the user doesn't own the resource +/// - `None` if the user is not authenticated +#[cfg(feature = "fullstack-server")] +pub fn use_is_owner_async( + resource_owner: ReadSignal>, +) -> Resource> { + let auth_state = use_context::>(); + let fetcher = use_context::(); + + use_resource(move || { + let fetcher = fetcher.clone(); + let owner = resource_owner(); + async move { + let current_did = auth_state.read().did.clone()?; + + match owner { + AtIdentifier::Did(did) => Some(did == current_did), + AtIdentifier::Handle(handle) => match fetcher.resolve_handle(&handle).await { + Ok(resolved_did) => Some(resolved_did == current_did), + Err(_) => Some(false), + }, + } + } + }) +} + +/// Check ownership with async handle resolution (client-only mode). +#[cfg(not(feature = "fullstack-server"))] +pub fn use_is_owner_async( + resource_owner: ReadSignal>, +) -> Resource> { + let auth_state = use_context::>(); + let fetcher = use_context::(); + + use_resource(move || { + let fetcher = fetcher.clone(); + let owner = resource_owner(); + async move { + let current_did = auth_state.read().did.clone()?; + + match owner { + AtIdentifier::Did(did) => Some(did == current_did), + AtIdentifier::Handle(handle) => match fetcher.resolve_handle(&handle).await { + Ok(resolved_did) => Some(resolved_did == current_did), + Err(_) => Some(false), + }, + } + } + }) +} + #[cfg(feature = "fullstack-server")] #[put("/cache/{ident}/{cid}?name", cache: Extension>)] pub async fn cache_blob(ident: SmolStr, cid: SmolStr, name: Option) -> Result<()> { diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index 2a64a0b..5cf7b0e 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -17,7 +17,9 @@ use jacquard::{ use std::sync::{Arc, LazyLock}; #[allow(unused)] use views::{ - Callback, Editor, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordPage, + Callback, DraftEdit, DraftsList, Editor, Home, Navbar, NewDraft, Notebook, NotebookEntryByRkey, + NotebookEntryEdit, NotebookIndex, NotebookPage, RecordIndex, RecordPage, StandaloneEntry, + StandaloneEntryEdit, }; use crate::{ @@ -69,12 +71,30 @@ enum Route { #[layout(Repository)] #[route("/")] RepositoryIndex { ident: AtIdentifier<'static> }, + // Drafts routes (before /:book_title to avoid capture) + #[route("/drafts")] + DraftsList { ident: AtIdentifier<'static> }, + #[route("/drafts/:tid")] + DraftEdit { ident: AtIdentifier<'static>, tid: SmolStr }, + #[route("/new?:notebook")] + NewDraft { ident: AtIdentifier<'static>, notebook: Option }, + // Standalone entry routes + #[route("/e/:rkey")] + StandaloneEntry { ident: AtIdentifier<'static>, rkey: SmolStr }, + #[route("/e/:rkey/edit")] + StandaloneEntryEdit { ident: AtIdentifier<'static>, rkey: SmolStr }, + // Notebook routes #[nest("/:book_title")] #[layout(Notebook)] #[route("/")] NotebookIndex { ident: AtIdentifier<'static>, book_title: SmolStr }, #[route("/:title")] - EntryPage { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } + EntryPage { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr }, + // Entry by rkey (canonical path) + #[route("/e/:rkey")] + NotebookEntryByRkey { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, + #[route("/e/:rkey/edit")] + NotebookEntryEdit { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr } } const FAVICON: Asset = asset!("/assets/weaver_photo_sm.jpg"); diff --git a/crates/weaver-app/src/views/drafts.rs b/crates/weaver-app/src/views/drafts.rs new file mode 100644 index 0000000..04b992b --- /dev/null +++ b/crates/weaver-app/src/views/drafts.rs @@ -0,0 +1,263 @@ +//! Drafts and standalone entry views. + +use crate::Route; +use crate::auth::AuthState; +use crate::components::button::{Button, ButtonVariant}; +use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; +use crate::components::editor::{delete_draft, list_drafts}; +use dioxus::prelude::*; +use jacquard::smol_str::SmolStr; +use jacquard::types::ident::AtIdentifier; + +const DRAFTS_CSS: Asset = asset!("/assets/styling/drafts.css"); + +/// Drafts list page - shows all drafts for the authenticated user. +#[component] +pub fn DraftsList(ident: ReadSignal>) -> Element { + // ALL hooks must be called unconditionally at the top + let auth_state = use_context::>(); + let navigator = use_navigator(); + let mut drafts = use_signal(list_drafts); + let mut show_delete_confirm = use_signal(|| None::); + + // Check ownership - redirect if not viewing own drafts + let current_did = auth_state.read().did.clone(); + let is_owner = match (¤t_did, ident()) { + (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did, + _ => false, + }; + + // Redirect non-owners + let ident_for_redirect = ident(); + use_effect(move || { + if !is_owner { + navigator.replace(Route::RepositoryIndex { + ident: ident_for_redirect.clone(), + }); + } + }); + + if !is_owner { + return rsx! { div { "Redirecting..." } }; + } + + let mut handle_delete = move |key: String| { + delete_draft(&key); + drafts.set(list_drafts()); + show_delete_confirm.set(None); + }; + + rsx! { + document::Link { rel: "stylesheet", href: DRAFTS_CSS } + document::Title { "Drafts" } + + div { class: "drafts-page", + div { class: "drafts-header", + h1 { "Drafts" } + Link { + to: Route::NewDraft { ident: ident(), notebook: None }, + Button { + variant: ButtonVariant::Primary, + "New Draft" + } + } + } + + if drafts().is_empty() { + div { class: "drafts-empty", + p { "No drafts yet." } + p { "Start writing something new!" } + } + } else { + div { class: "drafts-list", + for (key, title, editing_uri) in drafts() { + { + let key_for_delete = key.clone(); + let is_edit_draft = editing_uri.is_some(); + let display_title = if title.is_empty() { "Untitled".to_string() } else { title }; + let tid = key.strip_prefix("new:").unwrap_or(&key); + + rsx! { + div { + class: "draft-card", + key: "{key}", + + Link { + to: Route::DraftEdit { + ident: ident(), + tid: tid.to_string().into(), + }, + class: "draft-card-link", + + div { class: "draft-card-content", + h3 { class: "draft-title", "{display_title}" } + if is_edit_draft { + span { class: "draft-badge draft-badge-edit", "Editing" } + } else { + span { class: "draft-badge draft-badge-new", "New" } + } + } + } + + Button { + variant: ButtonVariant::Ghost, + onclick: move |_| show_delete_confirm.set(Some(key_for_delete.clone())), + "×" + } + } + } + } + } + } + } + } + + // Delete confirmation + DialogRoot { + open: show_delete_confirm().is_some(), + on_open_change: move |_: bool| show_delete_confirm.set(None), + DialogContent { + DialogTitle { "Delete Draft?" } + DialogDescription { + "This will permanently delete this draft." + } + div { class: "dialog-actions", + Button { + variant: ButtonVariant::Destructive, + onclick: move |_| { + if let Some(key) = show_delete_confirm() { + handle_delete(key); + } + }, + "Delete" + } + Button { + variant: ButtonVariant::Ghost, + onclick: move |_| show_delete_confirm.set(None), + "Cancel" + } + } + } + } + } +} + +/// Edit an existing draft by TID. +#[component] +pub fn DraftEdit( + ident: ReadSignal>, + tid: ReadSignal, +) -> Element { + use crate::components::editor::MarkdownEditor; + use crate::views::editor::EditorCss; + + // Draft key for "new" drafts is "new:{tid}" + let draft_key = format!("new:{}", tid()); + + rsx! { + EditorCss {} + div { class: "editor-page", + MarkdownEditor { entry_uri: Some(draft_key), target_notebook: None } + } + } +} + +/// Create a new draft. +#[component] +pub fn NewDraft( + ident: ReadSignal>, + notebook: ReadSignal>, +) -> Element { + use crate::components::editor::MarkdownEditor; + use crate::views::editor::EditorCss; + + rsx! { + EditorCss {} + div { class: "editor-page", + MarkdownEditor { + entry_uri: None, + target_notebook: notebook() + } + } + } +} + +/// View a standalone entry by rkey. +#[component] +pub fn StandaloneEntry( + ident: ReadSignal>, + rkey: ReadSignal, +) -> Element { + // Construct AT-URI for the entry + let entry_uri = use_memo(move || { + format!("at://{}/sh.weaver.notebook.entry/{}", ident(), rkey()) + }); + + rsx! { + div { class: "standalone-entry-page", + p { "Standalone entry view - implementation pending" } + p { "URI: {entry_uri}" } + } + } +} + +/// Edit a standalone entry. +#[component] +pub fn StandaloneEntryEdit( + ident: ReadSignal>, + rkey: ReadSignal, +) -> Element { + use crate::components::editor::MarkdownEditor; + use crate::views::editor::EditorCss; + + // Construct AT-URI for the entry + let entry_uri = use_memo(move || { + format!("at://{}/sh.weaver.notebook.entry/{}", ident(), rkey()) + }); + + rsx! { + EditorCss {} + div { class: "editor-page", + MarkdownEditor { entry_uri: Some(entry_uri()), target_notebook: None } + } + } +} + +/// View a notebook entry by rkey. +#[component] +pub fn NotebookEntryByRkey( + ident: ReadSignal>, + book_title: ReadSignal, + rkey: ReadSignal, +) -> Element { + rsx! { + div { class: "notebook-entry-rkey-page", + p { "Notebook entry by rkey - implementation pending" } + p { "Notebook: {book_title}" } + p { "Entry rkey: {rkey}" } + } + } +} + +/// Edit a notebook entry by rkey. +#[component] +pub fn NotebookEntryEdit( + ident: ReadSignal>, + book_title: ReadSignal, + rkey: ReadSignal, +) -> Element { + use crate::components::editor::MarkdownEditor; + use crate::views::editor::EditorCss; + + // Construct AT-URI for the entry + let entry_uri = use_memo(move || { + format!("at://{}/sh.weaver.notebook.entry/{}", ident(), rkey()) + }); + + rsx! { + EditorCss {} + div { class: "editor-page", + MarkdownEditor { entry_uri: Some(entry_uri()), target_notebook: Some(book_title()) } + } + } +} diff --git a/crates/weaver-app/src/views/editor.rs b/crates/weaver-app/src/views/editor.rs index 95522b1..413e939 100644 --- a/crates/weaver-app/src/views/editor.rs +++ b/crates/weaver-app/src/views/editor.rs @@ -12,7 +12,7 @@ pub fn Editor(entry: Option) -> Element { rsx! { EditorCss {} div { class: "editor-page", - MarkdownEditor { entry_uri: entry } + MarkdownEditor { entry_uri: entry, target_notebook: None } } } } diff --git a/crates/weaver-app/src/views/mod.rs b/crates/weaver-app/src/views/mod.rs index 43dca23..f02822a 100644 --- a/crates/weaver-app/src/views/mod.rs +++ b/crates/weaver-app/src/views/mod.rs @@ -28,3 +28,9 @@ pub use callback::Callback; mod editor; pub use editor::Editor; + +mod drafts; +pub use drafts::{ + DraftEdit, DraftsList, NewDraft, NotebookEntryByRkey, NotebookEntryEdit, StandaloneEntry, + StandaloneEntryEdit, +}; diff --git a/crates/weaver-app/src/views/notebook.rs b/crates/weaver-app/src/views/notebook.rs index 62493e8..80d460d 100644 --- a/crates/weaver-app/src/views/notebook.rs +++ b/crates/weaver-app/src/views/notebook.rs @@ -1,6 +1,8 @@ use crate::{ Route, + auth::AuthState, components::{EntryCard, NotebookCover, NotebookCss}, + components::button::{Button, ButtonVariant}, data, }; use dioxus::prelude::*; @@ -50,6 +52,16 @@ pub fn NotebookIndex( #[cfg(feature = "fullstack-server")] entries_result?; + // Check ownership for "Add Entry" button + let auth_state = use_context::>(); + let is_owner = { + let current_did = auth_state.read().did.clone(); + match (¤t_did, ident()) { + (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did, + _ => false, + } + }; + rsx! { document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } @@ -63,7 +75,9 @@ pub fn NotebookIndex( aside { class: "notebook-sidebar", NotebookCover { notebook: notebook_view.clone(), - title: book_title().to_string() + title: book_title().to_string(), + is_owner, + ident: Some(ident()) } } diff --git a/crates/weaver-common/src/agent.rs b/crates/weaver-common/src/agent.rs index 433dcab..7eef674 100644 --- a/crates/weaver-common/src/agent.rs +++ b/crates/weaver-common/src/agent.rs @@ -5,6 +5,7 @@ pub use weaver_api::sh_weaver::notebook::{ }; // Re-export jacquard for convenience +use crate::constellation::{GetBacklinksQuery, RecordId}; use crate::error::WeaverError; pub use jacquard; use jacquard::bytes::Bytes; @@ -14,8 +15,10 @@ use jacquard::prelude::*; use jacquard::types::blob::{BlobRef, MimeType}; use jacquard::types::string::{AtUri, Did, RecordKey}; use jacquard::types::tid::Tid; +use jacquard::types::uri::Uri; +use jacquard::url::Url; use jacquard::xrpc::Response; -use jacquard::{IntoStatic, xrpc}; +use jacquard::{CowStr, IntoStatic, xrpc}; use mime_sniffer::MimeTypeSniffer; use std::path::Path; use weaver_api::com_atproto::repo::get_record::GetRecordResponse; @@ -25,6 +28,8 @@ use weaver_api::sh_weaver::publish::blob::Blob as PublishedBlob; use crate::{PublishResult, W_TICKER, normalize_title_path}; +const CONSTELLATION_URL: &str = "https://constellation.microcosm.blue"; + /// Extension trait providing weaver-specific multi-step operations on Agent /// /// This trait extends jacquard's Agent with notebook-specific workflows that @@ -157,20 +162,27 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { AgentError::from(ClientError::from(e).with_context("Failed to resolve PDS for DID")) })?; - // Search for existing notebook with this title - let resp = self - .xrpc(pds_url) - .send( - &ListRecords::new() - .repo(author_did.clone()) - .collection(Nsid::raw(Book::NSID)) - .limit(100) - .build(), - ) - .await - .map_err(|e| AgentError::from(ClientError::from(e)))?; + // Search for existing notebook with this title (paginated) + let mut cursor: Option> = None; + loop { + let resp = self + .xrpc(pds_url.clone()) + .send( + &ListRecords::new() + .repo(author_did.clone()) + .collection(Nsid::raw(Book::NSID)) + .limit(100) + .maybe_cursor(cursor.clone()) + .build(), + ) + .await + .map_err(|e| AgentError::from(ClientError::from(e)))?; + + let list = match resp.parse() { + Ok(l) => l, + Err(_) => break, // Parse error, stop searching + }; - if let Ok(list) = resp.parse() { for record in list.records { let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { AgentError::from(ClientError::invalid_request( @@ -189,6 +201,11 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { return Ok((record.uri.into_static(), entries)); } } + + match list.cursor { + Some(c) => cursor = Some(c.into_static()), + None => break, // No more pages + } } // Notebook doesn't exist, create it @@ -235,7 +252,30 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { // Find or create notebook let (notebook_uri, entry_refs) = self.upsert_notebook(notebook_title, &did).await?; + // Fast path: if notebook is empty, skip search and create directly + if entry_refs.is_empty() { + let response = self.create_record(entry, None).await?; + let new_ref = StrongRef::new() + .uri(response.uri.clone().into_static()) + .cid(response.cid.clone().into_static()) + .build(); + + use weaver_api::sh_weaver::notebook::book::Book; + 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(notebook_entry_ref); + }) + .await?; + + return Ok((new_ref, true)); + } + // Check if entry with this title exists in the notebook + // O(n) network calls - unavoidable without title indexing for entry_ref in &entry_refs { let existing = self .get_record::(&entry_ref.uri) @@ -390,6 +430,9 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { } /// Search for an entry by title within a notebook's entry list + /// + /// O(n) network calls - unavoidable without title indexing. + /// Breaks early on match to minimize unnecessary fetches. fn entry_by_title<'a>( &self, notebook: &NotebookView<'a>, @@ -478,79 +521,51 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { } }; - // TODO: use the cursor to search through all records with this NSID for the repo - let resp = self - .xrpc(pds_url) - .send( - &ListRecords::new() - .repo(repo_did) - .collection(Nsid::raw(Book::NSID)) - .limit(100) - .build(), - ) - .await - .map_err(|e| AgentError::from(ClientError::from(e)))?; + // Search with pagination + let mut cursor: Option> = None; + loop { + let resp = self + .xrpc(pds_url.clone()) + .send( + &ListRecords::new() + .repo(repo_did.clone()) + .collection(Nsid::raw(Book::NSID)) + .limit(100) + .maybe_cursor(cursor.clone()) + .build(), + ) + .await + .map_err(|e| AgentError::from(ClientError::from(e)))?; + + let list = match resp.parse() { + Ok(l) => l, + Err(_) => break, + }; - if let Ok(list) = resp.parse() { for record in list.records { let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { AgentError::from(ClientError::invalid_request( "Failed to parse notebook record", )) })?; - if let Some(book_title) = notebook.path - && book_title == title - { - let tags = notebook.tags.clone(); - let mut authors = Vec::new(); - - for (index, author) in notebook.authors.iter().enumerate() { - let (profile_uri, profile_view) = - self.hydrate_profile_view(&author.did).await?; - authors.push( - AuthorListView::new() - .maybe_uri(profile_uri) - .record(profile_view) - .index(index as i64) - .build(), - ); - } - let entries = notebook - .entry_list - .iter() - .cloned() - .map(IntoStatic::into_static) - .collect(); - - return Ok(Some(( - NotebookView::new() - .cid(record.cid) - .uri(record.uri) - .indexed_at(jacquard::types::string::Datetime::now()) - .title(book_title) - .maybe_tags(tags) - .authors(authors) - .record(record.value.clone()) - .build() - .into_static(), - entries, - ))); - } else if let Some(book_title) = notebook.title - && book_title == title + // Match on path first, then title + let matched_title = if let Some(ref path) = notebook.path + && path.as_ref() == title + { + Some(path.clone()) + } else if let Some(ref book_title) = notebook.title + && book_title.as_ref() == title { + Some(book_title.clone()) + } else { + None + }; + + if let Some(matched) = matched_title { let tags = notebook.tags.clone(); let mut authors = Vec::new(); - use weaver_api::app_bsky::actor::{ - ProfileViewDetailed, get_profile::GetProfile, - profile::Profile as BskyProfile, - }; - use weaver_api::sh_weaver::actor::{ - ProfileDataView, ProfileDataViewInner, ProfileView, - profile::Profile as WeaverProfile, - }; - for (index, author) in notebook.authors.iter().enumerate() { let (profile_uri, profile_view) = self.hydrate_profile_view(&author.did).await?; @@ -562,6 +577,7 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { .build(), ); } + let entries = notebook .entry_list .iter() @@ -574,7 +590,7 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { .cid(record.cid) .uri(record.uri) .indexed_at(jacquard::types::string::Datetime::now()) - .title(book_title) + .title(matched) .maybe_tags(tags) .authors(authors) .record(record.value.clone()) @@ -584,6 +600,11 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { ))); } } + + match list.cursor { + Some(c) => cursor = Some(c.into_static()), + None => break, + } } Ok(None) @@ -905,6 +926,59 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { .build()) } } + + /// Find the notebook that contains a given entry using constellation backlinks. + /// + /// Queries constellation for `sh.weaver.notebook.book` records that reference + /// the given entry URI via the `.entryList[].uri` path. + fn find_notebook_for_entry( + &self, + entry_uri: &AtUri<'_>, + ) -> impl Future>, WeaverError>> + where + Self: Sized, + { + async move { + let constellation_url = Url::parse(CONSTELLATION_URL).map_err(|e| { + AgentError::from(ClientError::invalid_request(format!( + "Invalid constellation URL: {}", + e + ))) + })?; + + let query = GetBacklinksQuery { + subject: Uri::At(entry_uri.clone().into_static()), + source: "sh.weaver.notebook.book:.entryList[].uri".into(), + cursor: None, + did: vec![], + limit: 1, + }; + + let response = self + .xrpc(constellation_url) + .send(&query) + .await + .map_err(|e| { + AgentError::from(ClientError::invalid_request(format!( + "Constellation query failed: {}", + e + ))) + })?; + + let output = response.into_output().map_err(|e| { + AgentError::from(ClientError::invalid_request(format!( + "Failed to parse constellation response: {}", + e + ))) + })?; + + Ok(output + .records + .into_iter() + .next() + .map(|r| r.into_static())) + } + } } impl WeaverExt for T {} diff --git a/lexicons/collab/accept.json b/lexicons/collab/accept.json new file mode 100644 index 0000000..05d668f --- /dev/null +++ b/lexicons/collab/accept.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "sh.weaver.collab.accept", + "defs": { + "main": { + "type": "record", + "description": "Acceptance of a collaboration invite. Completes the two-way agreement.", + "key": "tid", + "record": { + "type": "object", + "required": ["invite", "resource", "createdAt"], + "properties": { + "invite": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "Reference to the invite record being accepted." + }, + "resource": { + "type": "string", + "format": "at-uri", + "description": "URI of the resource (denormalized for easier querying)." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/collab/defs.json b/lexicons/collab/defs.json new file mode 100644 index 0000000..271c88f --- /dev/null +++ b/lexicons/collab/defs.json @@ -0,0 +1,18 @@ +{ + "lexicon": 1, + "id": "sh.weaver.collab.defs", + "defs": { + "notebook": { + "type": "token", + "description": "Collaboration scoped to an entire notebook." + }, + "entry": { + "type": "token", + "description": "Collaboration scoped to a single entry." + }, + "chapter": { + "type": "token", + "description": "Collaboration scoped to a chapter." + } + } +} diff --git a/lexicons/collab/invite.json b/lexicons/collab/invite.json new file mode 100644 index 0000000..8889261 --- /dev/null +++ b/lexicons/collab/invite.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "sh.weaver.collab.invite", + "defs": { + "main": { + "type": "record", + "description": "Invitation to collaborate on a resource (notebook, entry, chapter, etc.). Creates half of a two-way agreement.", + "key": "tid", + "record": { + "type": "object", + "required": ["resource", "invitee", "createdAt"], + "properties": { + "resource": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "The resource to collaborate on (notebook, entry, chapter, etc.)." + }, + "invitee": { + "type": "string", + "format": "did", + "description": "DID of the user being invited." + }, + "scope": { + "type": "ref", + "ref": "#collabScope", + "description": "Optional explicit scope type. If omitted, inferred from resource lexicon." + }, + "message": { + "type": "string", + "maxGraphemes": 300, + "maxLength": 3000, + "description": "Optional message to the invitee." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "expiresAt": { + "type": "string", + "format": "datetime", + "description": "Optional expiration for the invite." + } + } + } + }, + "collabScope": { + "type": "string", + "description": "The scope/type of collaboration.", + "knownValues": [ + "sh.weaver.collab.defs#notebook", + "sh.weaver.collab.defs#entry", + "sh.weaver.collab.defs#chapter" + ] + } + } +} diff --git a/lexicons/edit/diff.json b/lexicons/edit/diff.json index c824c49..a683299 100644 --- a/lexicons/edit/diff.json +++ b/lexicons/edit/diff.json @@ -32,6 +32,10 @@ "doc": { "type": "ref", "ref": "sh.weaver.edit.defs#docRef" + }, + "createdAt": { + "type": "string", + "format": "datetime" } } } diff --git a/lexicons/graph/bookmark.json b/lexicons/graph/bookmark.json new file mode 100644 index 0000000..ec6c62d --- /dev/null +++ b/lexicons/graph/bookmark.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.bookmark", + "defs": { + "main": { + "type": "record", + "description": "Bookmark a notebook or entry for later reading.", + "key": "tid", + "record": { + "type": "object", + "required": ["subject", "createdAt"], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "The notebook or entry being bookmarked." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "note": { + "type": "string", + "maxGraphemes": 300, + "maxLength": 3000, + "description": "Optional private note about why you saved this." + } + } + } + } + } +} diff --git a/lexicons/graph/defs.json b/lexicons/graph/defs.json new file mode 100644 index 0000000..0767503 --- /dev/null +++ b/lexicons/graph/defs.json @@ -0,0 +1,14 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.defs", + "defs": { + "curatelist": { + "type": "token", + "description": "A curated collection of notebooks/entries for sharing." + }, + "readinglist": { + "type": "token", + "description": "A personal reading list." + } + } +} diff --git a/lexicons/graph/follow.json b/lexicons/graph/follow.json new file mode 100644 index 0000000..a5fcb86 --- /dev/null +++ b/lexicons/graph/follow.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.follow", + "defs": { + "main": { + "type": "record", + "description": "Request to follow an author. Requires acceptance to be active.", + "key": "tid", + "record": { + "type": "object", + "required": ["subject", "createdAt"], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "DID of the author to follow." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/graph/followAccept.json b/lexicons/graph/followAccept.json new file mode 100644 index 0000000..cdf3730 --- /dev/null +++ b/lexicons/graph/followAccept.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.followAccept", + "defs": { + "main": { + "type": "record", + "description": "Acceptance of a follow request. Completes the two-way agreement.", + "key": "tid", + "record": { + "type": "object", + "required": ["follow", "createdAt"], + "properties": { + "follow": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "Reference to the follow record being accepted." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/graph/followGate.json b/lexicons/graph/followGate.json new file mode 100644 index 0000000..9003677 --- /dev/null +++ b/lexicons/graph/followGate.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.followGate", + "defs": { + "main": { + "type": "record", + "description": "Settings controlling follow approval behavior. Absence means auto-accept.", + "key": "self", + "record": { + "type": "object", + "required": ["createdAt"], + "properties": { + "requireApproval": { + "type": "boolean", + "default": false, + "description": "If true, follows require manual acceptance." + }, + "invalidatePrior": { + "type": "boolean", + "default": false, + "description": "If true, previously auto-accepted follows are invalidated when requireApproval is enabled. Appview should treat followAccept records created before this gate's createdAt as invalid." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/graph/like.json b/lexicons/graph/like.json new file mode 100644 index 0000000..2029320 --- /dev/null +++ b/lexicons/graph/like.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.like", + "defs": { + "main": { + "type": "record", + "description": "Record declaring a 'like' of a notebook or entry.", + "key": "tid", + "record": { + "type": "object", + "required": ["subject", "createdAt"], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "The notebook or entry being liked." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/graph/list.json b/lexicons/graph/list.json new file mode 100644 index 0000000..a57fc56 --- /dev/null +++ b/lexicons/graph/list.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.list", + "defs": { + "main": { + "type": "record", + "description": "A curated list of notebooks and/or entries.", + "key": "tid", + "record": { + "type": "object", + "required": ["name", "purpose", "createdAt"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Display name for the list." + }, + "purpose": { + "type": "ref", + "ref": "#listPurpose", + "description": "The purpose/type of list." + }, + "description": { + "type": "string", + "maxGraphemes": 300, + "maxLength": 3000 + }, + "avatar": { + "type": "blob", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + }, + "listPurpose": { + "type": "string", + "knownValues": [ + "sh.weaver.graph.defs#curatelist", + "sh.weaver.graph.defs#readinglist" + ] + } + } +} diff --git a/lexicons/graph/listitem.json b/lexicons/graph/listitem.json new file mode 100644 index 0000000..5432c0e --- /dev/null +++ b/lexicons/graph/listitem.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.listitem", + "defs": { + "main": { + "type": "record", + "description": "An item in a list.", + "key": "tid", + "record": { + "type": "object", + "required": ["subject", "list", "createdAt"], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "The notebook or entry being added to the list." + }, + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference to the list record." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/graph/subscribe.json b/lexicons/graph/subscribe.json new file mode 100644 index 0000000..19c3f4c --- /dev/null +++ b/lexicons/graph/subscribe.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.subscribe", + "defs": { + "main": { + "type": "record", + "description": "Request to subscribe to a notebook. Requires acceptance to be active.", + "key": "tid", + "record": { + "type": "object", + "required": ["notebook", "createdAt"], + "properties": { + "notebook": { + "type": "string", + "format": "at-uri", + "description": "URI of the notebook to subscribe to." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/graph/subscribeAccept.json b/lexicons/graph/subscribeAccept.json new file mode 100644 index 0000000..ad70d4c --- /dev/null +++ b/lexicons/graph/subscribeAccept.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "sh.weaver.graph.subscribeAccept", + "defs": { + "main": { + "type": "record", + "description": "Acceptance of a subscription request.", + "key": "tid", + "record": { + "type": "object", + "required": ["subscribe", "createdAt"], + "properties": { + "subscribe": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "Reference to the subscribe record being accepted." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/lexicons/notebook/book.json b/lexicons/notebook/book.json index 8de8b00..2ba1de8 100644 --- a/lexicons/notebook/book.json +++ b/lexicons/notebook/book.json @@ -35,6 +35,11 @@ "type": "string", "format": "datetime", "description": "Client-declared timestamp when this was originally created." + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios." } } } diff --git a/lexicons/notebook/entry.json b/lexicons/notebook/entry.json index acca88e..1396d2f 100644 --- a/lexicons/notebook/entry.json +++ b/lexicons/notebook/entry.json @@ -25,6 +25,11 @@ "format": "datetime", "description": "Client-declared timestamp when this was originally created." }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios." + }, "embeds": { "type": "object", "description": "The set of images and records, if any, embedded in the notebook entry.", diff --git a/lexicons/notebook/getEntry.json b/lexicons/notebook/getEntry.json new file mode 100644 index 0000000..68debd5 --- /dev/null +++ b/lexicons/notebook/getEntry.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getEntry", + "defs": { + "main": { + "type": "query", + "description": "Get an entry view by notebook URI and index, including prev/next navigation.", + "parameters": { + "type": "params", + "required": ["notebook"], + "properties": { + "notebook": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the notebook containing the entry." + }, + "index": { + "type": "integer", + "minimum": 0, + "default": 0, + "description": "Zero-based index of the entry in the notebook's entry list." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#bookEntryView" + } + }, + "errors": [{ "name": "NotebookNotFound" }, { "name": "EntryNotFound" }] + } + } +} diff --git a/lexicons/notebook/getEntryByTitle.json b/lexicons/notebook/getEntryByTitle.json new file mode 100644 index 0000000..a4dba49 --- /dev/null +++ b/lexicons/notebook/getEntryByTitle.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getEntryByTitle", + "defs": { + "main": { + "type": "query", + "description": "Get an entry view by notebook URI and title. Matches on either the entry's path or title field.", + "parameters": { + "type": "params", + "required": ["notebook", "title"], + "properties": { + "notebook": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the notebook containing the entry." + }, + "title": { + "type": "string", + "maxLength": 300, + "description": "Title or path of the entry to fetch." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["entry", "record"], + "properties": { + "entry": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#bookEntryView" + }, + "record": { + "type": "unknown", + "description": "The raw entry record data." + } + } + } + }, + "errors": [ + { "name": "NotebookNotFound" }, + { "name": "EntryNotFound" } + ] + } + } +} diff --git a/lexicons/notebook/getNotebook.json b/lexicons/notebook/getNotebook.json new file mode 100644 index 0000000..314886c --- /dev/null +++ b/lexicons/notebook/getNotebook.json @@ -0,0 +1,41 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getNotebook", + "defs": { + "main": { + "type": "query", + "description": "Get a notebook view by its AT-URI, including hydrated author profiles and entry list.", + "parameters": { + "type": "params", + "required": ["notebook"], + "properties": { + "notebook": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the notebook to fetch." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["notebook", "entries"], + "properties": { + "notebook": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#notebookView" + }, + "entries": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + } + } + } +} diff --git a/lexicons/notebook/getNotebookByTitle.json b/lexicons/notebook/getNotebookByTitle.json new file mode 100644 index 0000000..b075f48 --- /dev/null +++ b/lexicons/notebook/getNotebookByTitle.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.getNotebookByTitle", + "defs": { + "main": { + "type": "query", + "description": "Get a notebook view by actor and title. Matches on either the notebook's path or title field.", + "parameters": { + "type": "params", + "required": ["actor", "title"], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Handle or DID of the notebook owner." + }, + "title": { + "type": "string", + "maxLength": 300, + "description": "Title or path of the notebook to fetch." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["notebook", "entries"], + "properties": { + "notebook": { + "type": "ref", + "ref": "sh.weaver.notebook.defs#notebookView" + }, + "entries": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + }, + "errors": [ + { "name": "NotebookNotFound" } + ] + } + } +} -- knotmirror 0.0.1