Compare commits
No commits in common. "wip/chunker" and "master" have entirely different histories.
wip/chunke
...
master
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,7 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"flabk",
|
||||
"flabk-derive",
|
||||
]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "flabk-derive"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full"] }
|
||||
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
|
@ -0,0 +1,22 @@
|
|||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
#[proc_macro_derive(LD)]
|
||||
pub fn derive_ld(input: TokenStream) -> TokenStream {
|
||||
let DeriveInput { ident, .. } = parse_macro_input!(input);
|
||||
quote! {
|
||||
impl crate::astreams::serde_ext::LDObject for #ident {
|
||||
fn from_iri(s: &str) -> Self {
|
||||
let mut ident = #ident::default();
|
||||
ident.id = s.into();
|
||||
ident
|
||||
}
|
||||
fn get_iri(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
46
flabk.go
46
flabk.go
|
@ -1,46 +0,0 @@
|
|||
package main
|
||||
|
||||
import "sectorinf.com/emilis/flabk/flabk/flabweb"
|
||||
|
||||
// MUST (client and server) application/ld+json; profile="https://www.w3.org/ns/activitystreams
|
||||
// SHOULD application/activity+json
|
||||
|
||||
// public actor https://www.w3.org/ns/activitystreams#Public
|
||||
|
||||
func main() {
|
||||
panic(flabweb.New("my.site").Run())
|
||||
}
|
||||
|
||||
// {
|
||||
// "@context": ["https://www.w3.org/ns/activitystreams",
|
||||
// {"@language": "en"}],
|
||||
// "type": "Like",
|
||||
// "actor": "https://dustycloud.org/chris/",
|
||||
// "name": "Chris liked 'Minimal ActivityPub update client'",
|
||||
// "object": "https://rhiaro.co.uk/2016/05/minimal-activitypub",
|
||||
// "to": ["https://rhiaro.co.uk/#amy",
|
||||
// "https://dustycloud.org/followers",
|
||||
// "https://rhiaro.co.uk/followers/"],
|
||||
// "cc": "https://e14n.com/evan"
|
||||
// }
|
||||
|
||||
// {
|
||||
// "@context": [
|
||||
// "https://www.w3.org/ns/activitystreams",
|
||||
// {
|
||||
// "@language": "en"
|
||||
// }
|
||||
// ],
|
||||
// "type": "Like",
|
||||
// "to": [
|
||||
// "https://rhiaro.co.uk/#amy",
|
||||
// "https://dustycloud.org/followers",
|
||||
// "https://rhiaro.co.uk/followers/"
|
||||
// ],
|
||||
// "cc": [
|
||||
// "https://e14n.com/evan"
|
||||
// ],
|
||||
// "object": "https://rhiaro.co.uk/2016/05/minimal-activitypub",
|
||||
// "actor": "https://dustycloud.org/chris/",
|
||||
// "name": "Chris liked 'Minimal ActivityPub update client'"
|
||||
// }
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'flabk'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=flabk",
|
||||
"--package=flabk"
|
||||
],
|
||||
"filter": {
|
||||
"name": "flabk",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
// {
|
||||
// "type": "lldb",
|
||||
// "request": "launch",
|
||||
// "name": "Debug unit tests in executable 'flabk'",
|
||||
// "cargo": {
|
||||
// "args": [
|
||||
// "test",
|
||||
// "--no-run",
|
||||
// "--bin=flabk",
|
||||
// "--package=flabk"
|
||||
// ],
|
||||
// "filter": {
|
||||
// "name": "flabk",
|
||||
// "kind": "bin"
|
||||
// }
|
||||
// },
|
||||
// "args": [],
|
||||
// "cwd": "${workspaceFolder}"
|
||||
// }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "flabk"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.64"
|
||||
argon2 = "0.4.1"
|
||||
async-trait = "0.1.58"
|
||||
axum = "0.5.17"
|
||||
base-62 = "0.1.1"
|
||||
handlebars = "4.3.3"
|
||||
jsonwebtoken = "8.1.1"
|
||||
mime_guess = "2.0.4"
|
||||
rand = "0.8.5"
|
||||
rand_core = { version = "0.6.3", features = ["std"] }
|
||||
reqwest = { version = "0.11.12", features = [
|
||||
"__tls",
|
||||
"default-tls",
|
||||
"hyper-tls",
|
||||
"native-tls-crate",
|
||||
"tokio-native-tls",
|
||||
"serde_json",
|
||||
"json",
|
||||
] }
|
||||
rust-embed = "6.4.0"
|
||||
serde = { version = "1.0.144", features = ["derive", "std", "serde_derive"] }
|
||||
serde_json = "1.0.85"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-postgres = { version = "0.7.7", features = ["with-serde_json-1"] }
|
||||
tower-cookies = "0.7.0"
|
||||
flabk-derive = { path = "../flabk-derive" }
|
|
@ -1,36 +0,0 @@
|
|||
package ap
|
||||
|
||||
import (
|
||||
"github.com/go-ap/jsonld"
|
||||
)
|
||||
|
||||
type Object struct {
|
||||
Context jsonld.Context `jsonld:"@context,omitempty,collapsible"`
|
||||
Type string `jsonld:"type"`
|
||||
Type2 string `jsonld:"@type"`
|
||||
ID jsonld.IRI `jsonld:"id,omitempty"`
|
||||
Name string `jsonld:"name,omitempty"`
|
||||
Source *Source `jsonld:"source,omitempty"`
|
||||
To []Object `jsonld:"to,omitempty,collapsible"`
|
||||
CC []Object `jsonld:"cc,omitempty,collapsible"`
|
||||
Object *Object `jsonld:"object,omitempty,collapsible"`
|
||||
Actor *Actor `jsonld:"actor,omitempty,collapsible"`
|
||||
}
|
||||
|
||||
func (o Object) Collapse() interface{} {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
type Actor struct {
|
||||
ID jsonld.IRI `jsonld:"id,omitempty"`
|
||||
Name string `jsonld:"name,omitempty"`
|
||||
}
|
||||
|
||||
func (a Actor) Collapse() interface{} {
|
||||
return a.ID
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Content string `jsonld:"content,omitempty"`
|
||||
MediaType string `jsonld:"mediaType,omitempty"`
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package ap_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-ap/jsonld"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sectorinf.com/emilis/flabk/flabk/ap"
|
||||
)
|
||||
|
||||
func GetExample() ap.Object {
|
||||
return ap.Object{
|
||||
Context: jsonld.Context{
|
||||
{
|
||||
IRI: jsonld.IRI("https://www.w3.org/ns/activitystreams"),
|
||||
},
|
||||
{
|
||||
Term: jsonld.LanguageKw,
|
||||
IRI: "en",
|
||||
},
|
||||
},
|
||||
Type: "Like",
|
||||
Actor: &ap.Actor{
|
||||
ID: "https://dustycloud.org/chris/",
|
||||
},
|
||||
Name: "Chris liked 'Minimal ActivityPub update client'",
|
||||
Object: &ap.Object{
|
||||
ID: "https://rhiaro.co.uk/2016/05/minimal-activitypub",
|
||||
},
|
||||
To: []ap.Object{
|
||||
{ID: "https://rhiaro.co.uk/#amy"},
|
||||
{ID: "https://dustycloud.org/followers"},
|
||||
{ID: "https://rhiaro.co.uk/followers/"},
|
||||
},
|
||||
CC: []ap.Object{
|
||||
{ID: "https://e14n.com/evan"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestObject(t *testing.T) {
|
||||
that := require.New(t)
|
||||
result, err := jsonld.Marshal(GetExample())
|
||||
that.NoError(err)
|
||||
fmt.Println(string(result))
|
||||
fmt.Println()
|
||||
another := ap.Object{}
|
||||
err = jsonld.Unmarshal(result, &another)
|
||||
that.NoError(err)
|
||||
fmt.Printf("%+v\n", another)
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="130.52438mm"
|
||||
height="130.52438mm"
|
||||
viewBox="0 0 130.52438 130.52438"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
|
||||
sodipodi:docname="flabk_icon_placeholder.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.54866744"
|
||||
inkscape:cx="-78.371699"
|
||||
inkscape:cy="260.63147"
|
||||
inkscape:window-width="1914"
|
||||
inkscape:window-height="1041"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer2" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<rect
|
||||
x="238.94496"
|
||||
y="545.79144"
|
||||
width="291.11197"
|
||||
height="295.74161"
|
||||
id="rect1130" />
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
inkscape:label="Roughen"
|
||||
id="filter1273"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1"
|
||||
height="1">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
numOctaves="5"
|
||||
seed="145"
|
||||
baseFrequency="0.001 10"
|
||||
result="turbulence"
|
||||
id="feTurbulence1269" />
|
||||
<feDisplacementMap
|
||||
in="SourceGraphic"
|
||||
in2="turbulence"
|
||||
scale="1.68439"
|
||||
yChannelSelector="G"
|
||||
xChannelSelector="R"
|
||||
id="feDisplacementMap1271" />
|
||||
</filter>
|
||||
<rect
|
||||
x="238.94496"
|
||||
y="545.79144"
|
||||
width="291.11197"
|
||||
height="295.74161"
|
||||
id="rect1130-3" />
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
inkscape:label="Roughen"
|
||||
id="filter1273-6"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1"
|
||||
height="1">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
numOctaves="5"
|
||||
seed="145"
|
||||
baseFrequency="0.001 10"
|
||||
result="turbulence"
|
||||
id="feTurbulence1269-7" />
|
||||
<feDisplacementMap
|
||||
in="SourceGraphic"
|
||||
in2="turbulence"
|
||||
scale="1.68439"
|
||||
yChannelSelector="G"
|
||||
xChannelSelector="R"
|
||||
id="feDisplacementMap1271-5" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(-38.320595,-110.90324)">
|
||||
<circle
|
||||
style="display:inline;fill:#663399;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path1074"
|
||||
cx="103.58279"
|
||||
cy="176.16544"
|
||||
r="65.262192" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 2"
|
||||
transform="translate(-38.320595,-110.90324)">
|
||||
<circle
|
||||
style="display:inline;fill:#0f0617;fill-opacity:1;stroke-width:0.224394;stroke-dasharray:none"
|
||||
id="path1074-5"
|
||||
cx="103.58279"
|
||||
cy="176.16544"
|
||||
r="55.262001" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Layer 3"
|
||||
transform="translate(-38.320595,-110.90324)">
|
||||
<g
|
||||
id="g2829"
|
||||
transform="matrix(0.88602282,0,0,0.87606946,13.533885,21.962211)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(2.0837961,0,0,2.645293,-442.06366,-1321.3309)"
|
||||
id="text1128-3"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1130-3);display:inline;fill:#482464;fill-opacity:1;stroke:none;stroke-width:1.00008;stroke-dasharray:none;filter:url(#filter1273-6)"><tspan
|
||||
x="238.94531"
|
||||
y="581.18164"
|
||||
id="tspan2868">fK</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(2.0837961,0,0,2.645293,-438.16349,-1321.0342)"
|
||||
id="text1128"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1130);fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter1273)"><tspan
|
||||
x="238.94531"
|
||||
y="581.18164"
|
||||
id="tspan2870">fK</tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
|
@ -1,46 +0,0 @@
|
|||
package flabweb
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"sectorinf.com/emilis/flabk/util/uriutil"
|
||||
)
|
||||
|
||||
const (
|
||||
EndpointOutbox = "outbox"
|
||||
)
|
||||
|
||||
var (
|
||||
tempOutboxIDCauseImNotStoringThis = 0
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
router *gin.Engine
|
||||
v1 *gin.RouterGroup
|
||||
hostname string
|
||||
}
|
||||
|
||||
func New(hostname string) Server {
|
||||
router := gin.Default()
|
||||
v1 := router.Group("v1")
|
||||
server := Server{
|
||||
hostname: hostname,
|
||||
router: router,
|
||||
v1: v1,
|
||||
}
|
||||
v1.POST(EndpointOutbox, server.Outbox)
|
||||
return server
|
||||
}
|
||||
|
||||
func (s Server) Run() error {
|
||||
return s.router.Run(":8081")
|
||||
}
|
||||
|
||||
func (s Server) Outbox(ctx *gin.Context) {
|
||||
id := tempOutboxIDCauseImNotStoringThis
|
||||
tempOutboxIDCauseImNotStoringThis++
|
||||
ctx.Header("Location", uriutil.JoinURIs(s.hostname, EndpointOutbox, strconv.Itoa(id)))
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::serde_ext::LDObject;
|
||||
|
||||
pub const CONTEXT_ID: &str = "https://www.w3.org/ns/activitystreams";
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize)]
|
||||
pub struct APContext {
|
||||
pub id: Option<String>,
|
||||
#[serde(rename = "@context")]
|
||||
pub ctx: ContextMap,
|
||||
}
|
||||
|
||||
impl LDObject for APContext {
|
||||
fn from_iri(iri: &str) -> Self {
|
||||
let mut ctx = Self::default();
|
||||
ctx.id = Some(iri.into());
|
||||
ctx
|
||||
}
|
||||
|
||||
fn get_iri(&self) -> String {
|
||||
self.id.clone().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ContextMap {
|
||||
#[serde(rename = "@vocab")]
|
||||
pub vocab: String,
|
||||
#[serde(rename = "xsd")]
|
||||
pub xsd: String,
|
||||
#[serde(rename = "as")]
|
||||
pub as_field: String,
|
||||
#[serde(rename = "ldp")]
|
||||
pub ldp: String,
|
||||
#[serde(rename = "vcard")]
|
||||
pub vcard: String,
|
||||
#[serde(rename = "id")]
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: String,
|
||||
pub accept: String,
|
||||
pub activity: String,
|
||||
pub intransitive_activity: String,
|
||||
pub add: String,
|
||||
pub announce: String,
|
||||
pub application: String,
|
||||
pub arrive: String,
|
||||
pub article: String,
|
||||
pub audio: String,
|
||||
pub block: String,
|
||||
pub collection: String,
|
||||
pub collection_page: String,
|
||||
pub relationship: String,
|
||||
pub create: String,
|
||||
pub delete: String,
|
||||
pub dislike: String,
|
||||
pub document: String,
|
||||
pub event: String,
|
||||
pub follow: String,
|
||||
pub flag: String,
|
||||
pub group: String,
|
||||
pub ignore: String,
|
||||
pub image: String,
|
||||
pub invite: String,
|
||||
pub join: String,
|
||||
pub leave: String,
|
||||
pub like: String,
|
||||
pub link: String,
|
||||
pub mention: String,
|
||||
pub note: String,
|
||||
pub object: String,
|
||||
pub offer: String,
|
||||
pub ordered_collection: String,
|
||||
pub ordered_collection_page: String,
|
||||
pub organization: String,
|
||||
pub page: String,
|
||||
pub person: String,
|
||||
pub place: String,
|
||||
pub profile: String,
|
||||
pub question: String,
|
||||
pub reject: String,
|
||||
pub remove: String,
|
||||
pub service: String,
|
||||
pub tentative_accept: String,
|
||||
pub tentative_reject: String,
|
||||
pub tombstone: String,
|
||||
pub undo: String,
|
||||
pub update: String,
|
||||
pub video: String,
|
||||
pub view: String,
|
||||
pub listen: String,
|
||||
pub read: String,
|
||||
#[serde(rename = "Move")]
|
||||
pub move_field: String,
|
||||
pub travel: String,
|
||||
pub is_following: String,
|
||||
pub is_followed_by: String,
|
||||
pub is_contact: String,
|
||||
pub is_member: String,
|
||||
#[serde(rename = "subject")]
|
||||
pub subject: Option<TypedField>,
|
||||
#[serde(rename = "relationship")]
|
||||
pub relationship2: Option<TypedField>,
|
||||
#[serde(rename = "actor")]
|
||||
pub actor: Option<TypedField>,
|
||||
#[serde(rename = "attributedTo")]
|
||||
pub attributed_to: Option<TypedField>,
|
||||
#[serde(rename = "attachment")]
|
||||
pub attachment: Option<TypedField>,
|
||||
#[serde(rename = "bcc")]
|
||||
pub bcc: Option<TypedField>,
|
||||
#[serde(rename = "bto")]
|
||||
pub bto: Option<TypedField>,
|
||||
#[serde(rename = "cc")]
|
||||
pub cc: Option<TypedField>,
|
||||
#[serde(rename = "context")]
|
||||
pub context: Option<TypedField>,
|
||||
#[serde(rename = "current")]
|
||||
pub current: Option<TypedField>,
|
||||
#[serde(rename = "first")]
|
||||
pub first: Option<TypedField>,
|
||||
#[serde(rename = "generator")]
|
||||
pub generator: Option<TypedField>,
|
||||
#[serde(rename = "icon")]
|
||||
pub icon: Option<TypedField>,
|
||||
#[serde(rename = "image")]
|
||||
pub image2: Option<TypedField>,
|
||||
#[serde(rename = "inReplyTo")]
|
||||
pub in_reply_to: Option<TypedField>,
|
||||
#[serde(rename = "items")]
|
||||
pub items: Option<TypedField>,
|
||||
#[serde(rename = "instrument")]
|
||||
pub instrument: Option<TypedField>,
|
||||
#[serde(rename = "orderedItems")]
|
||||
pub ordered_items: Option<TypedField>,
|
||||
#[serde(rename = "last")]
|
||||
pub last: Option<TypedField>,
|
||||
#[serde(rename = "location")]
|
||||
pub location: Option<TypedField>,
|
||||
#[serde(rename = "next")]
|
||||
pub next: Option<TypedField>,
|
||||
#[serde(rename = "object")]
|
||||
pub object2: Option<TypedField>,
|
||||
#[serde(rename = "oneOf")]
|
||||
pub one_of: Option<TypedField>,
|
||||
#[serde(rename = "anyOf")]
|
||||
pub any_of: Option<TypedField>,
|
||||
#[serde(rename = "closed")]
|
||||
pub closed: Option<TypedField>,
|
||||
#[serde(rename = "origin")]
|
||||
pub origin: Option<TypedField>,
|
||||
#[serde(rename = "accuracy")]
|
||||
pub accuracy: Option<TypedField>,
|
||||
#[serde(rename = "prev")]
|
||||
pub prev: Option<TypedField>,
|
||||
#[serde(rename = "preview")]
|
||||
pub preview: Option<TypedField>,
|
||||
#[serde(rename = "replies")]
|
||||
pub replies: Option<TypedField>,
|
||||
#[serde(rename = "result")]
|
||||
pub result: Option<TypedField>,
|
||||
#[serde(rename = "audience")]
|
||||
pub audience: Option<TypedField>,
|
||||
#[serde(rename = "partOf")]
|
||||
pub part_of: Option<TypedField>,
|
||||
#[serde(rename = "tag")]
|
||||
pub tag: Option<TypedField>,
|
||||
#[serde(rename = "target")]
|
||||
pub target: Option<TypedField>,
|
||||
#[serde(rename = "to")]
|
||||
pub to: Option<TypedField>,
|
||||
#[serde(rename = "url")]
|
||||
pub url: Option<TypedField>,
|
||||
#[serde(rename = "altitude")]
|
||||
pub altitude: Option<TypedField>,
|
||||
#[serde(rename = "content")]
|
||||
pub content: String,
|
||||
#[serde(rename = "contentMap")]
|
||||
pub content_map: Option<TypedField>,
|
||||
#[serde(rename = "name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "nameMap")]
|
||||
pub name_map: Option<TypedField>,
|
||||
#[serde(rename = "duration")]
|
||||
pub duration: Option<TypedField>,
|
||||
#[serde(rename = "endTime")]
|
||||
pub end_time: Option<TypedField>,
|
||||
#[serde(rename = "height")]
|
||||
pub height: Option<TypedField>,
|
||||
#[serde(rename = "href")]
|
||||
pub href: Option<TypedField>,
|
||||
#[serde(rename = "hreflang")]
|
||||
pub hreflang: String,
|
||||
#[serde(rename = "latitude")]
|
||||
pub latitude: Option<TypedField>,
|
||||
#[serde(rename = "longitude")]
|
||||
pub longitude: Option<TypedField>,
|
||||
#[serde(rename = "mediaType")]
|
||||
pub media_type: String,
|
||||
#[serde(rename = "published")]
|
||||
pub published: Option<TypedField>,
|
||||
#[serde(rename = "radius")]
|
||||
pub radius: Option<TypedField>,
|
||||
#[serde(rename = "rel")]
|
||||
pub rel: String,
|
||||
#[serde(rename = "startIndex")]
|
||||
pub start_index: Option<TypedField>,
|
||||
#[serde(rename = "startTime")]
|
||||
pub start_time: Option<TypedField>,
|
||||
#[serde(rename = "summary")]
|
||||
pub summary: String,
|
||||
#[serde(rename = "summaryMap")]
|
||||
pub summary_map: Option<TypedField>,
|
||||
#[serde(rename = "totalItems")]
|
||||
pub total_items: Option<TypedField>,
|
||||
#[serde(rename = "units")]
|
||||
pub units: String,
|
||||
#[serde(rename = "updated")]
|
||||
pub updated: Option<TypedField>,
|
||||
#[serde(rename = "width")]
|
||||
pub width: Option<TypedField>,
|
||||
#[serde(rename = "describes")]
|
||||
pub describes: Option<TypedField>,
|
||||
#[serde(rename = "formerType")]
|
||||
pub former_type: Option<TypedField>,
|
||||
#[serde(rename = "deleted")]
|
||||
pub deleted: Option<TypedField>,
|
||||
#[serde(rename = "inbox")]
|
||||
pub inbox: Option<TypedField>,
|
||||
#[serde(rename = "outbox")]
|
||||
pub outbox: Option<TypedField>,
|
||||
#[serde(rename = "following")]
|
||||
pub following: Option<TypedField>,
|
||||
#[serde(rename = "followers")]
|
||||
pub followers: Option<TypedField>,
|
||||
#[serde(rename = "streams")]
|
||||
pub streams: Option<TypedField>,
|
||||
#[serde(rename = "preferredUsername")]
|
||||
pub preferred_username: String,
|
||||
#[serde(rename = "endpoints")]
|
||||
pub endpoints: Option<TypedField>,
|
||||
#[serde(rename = "uploadMedia")]
|
||||
pub upload_media: Option<TypedField>,
|
||||
#[serde(rename = "proxyUrl")]
|
||||
pub proxy_url: Option<TypedField>,
|
||||
#[serde(rename = "liked")]
|
||||
pub liked: Option<TypedField>,
|
||||
#[serde(rename = "oauthAuthorizationEndpoint")]
|
||||
pub oauth_authorization_endpoint: Option<TypedField>,
|
||||
#[serde(rename = "oauthTokenEndpoint")]
|
||||
pub oauth_token_endpoint: Option<TypedField>,
|
||||
#[serde(rename = "provideClientKey")]
|
||||
pub provide_client_key: Option<TypedField>,
|
||||
#[serde(rename = "signClientKey")]
|
||||
pub sign_client_key: Option<TypedField>,
|
||||
#[serde(rename = "sharedInbox")]
|
||||
pub shared_inbox: Option<TypedField>,
|
||||
pub public: Option<TypedField>,
|
||||
#[serde(rename = "source")]
|
||||
pub source: String,
|
||||
#[serde(rename = "likes")]
|
||||
pub likes: Option<TypedField>,
|
||||
#[serde(rename = "shares")]
|
||||
pub shares: Option<TypedField>,
|
||||
#[serde(rename = "alsoKnownAs")]
|
||||
pub also_known_as: Option<TypedField>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize)]
|
||||
pub struct TypedField {
|
||||
#[serde(rename = "@id")]
|
||||
pub id: String,
|
||||
#[serde(rename = "@type")]
|
||||
pub kind: Option<String>,
|
||||
#[serde(rename = "@container")]
|
||||
pub container: Option<String>,
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use flabk_derive::LD;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::astreams::resolve::Resolvable;
|
||||
|
||||
use self::{
|
||||
resolve::ResolveError,
|
||||
serde_ext::{expand_partial, expand_partial_into_vec, into_vec},
|
||||
};
|
||||
|
||||
pub mod context;
|
||||
pub mod resolve;
|
||||
mod serde_ext;
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||
pub enum ObjectKind {
|
||||
Article,
|
||||
Audio,
|
||||
Document,
|
||||
Event,
|
||||
Image,
|
||||
Note,
|
||||
Page,
|
||||
Place,
|
||||
Profile,
|
||||
Relationship,
|
||||
Tombstone,
|
||||
Video,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||
pub enum ActivityKind {
|
||||
Accept,
|
||||
Add,
|
||||
Announce,
|
||||
Arrive,
|
||||
Block,
|
||||
Create,
|
||||
Delete,
|
||||
Dislike,
|
||||
Flag,
|
||||
Follow,
|
||||
Invite,
|
||||
Join,
|
||||
Leave,
|
||||
Like,
|
||||
Listen,
|
||||
Move,
|
||||
Offer,
|
||||
Question,
|
||||
Reject,
|
||||
Read,
|
||||
Remove,
|
||||
TentativeReject,
|
||||
TentativeAccept,
|
||||
Travel,
|
||||
Undo,
|
||||
Update,
|
||||
View,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExpandError {
|
||||
InvalidKind(Option<ActivityKind>),
|
||||
NoAttribution,
|
||||
ResolveIRI(String),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<ResolveError> for ExpandError {
|
||||
fn from(err: ResolveError) -> Self {
|
||||
Self::ResolveIRI(err.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Collection is a subtype of Object that
|
||||
/// represents ordered or unordered sets of
|
||||
/// Object or Link instances.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Collection<T> {
|
||||
pub summary: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
#[serde(rename = "totalItems")]
|
||||
pub total_items: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Attachment {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
pub content: Option<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
// TODO: maybe this can work as attachment w/ the ID being more like
|
||||
// the LD IDs?
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Image {
|
||||
#[serde(flatten)]
|
||||
pub base: ObjectBase,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<ObjectKind>,
|
||||
pub id: String,
|
||||
#[serde(deserialize_with = "into_vec")]
|
||||
pub url: Vec<Link>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Audience {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
pub enum Unit {
|
||||
#[serde(rename = "m")]
|
||||
Meters,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Location {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
pub longitude: Option<f64>,
|
||||
pub latitude: Option<f64>,
|
||||
pub altitude: Option<f64>,
|
||||
pub units: Unit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Preview {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
pub duration: Option<String>,
|
||||
pub url: Link,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct APResult {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// ObjectBase contains base Activity Streams
|
||||
/// members that all objects should have. Except kind.
|
||||
///
|
||||
/// This object is intended to be used by inlining it into
|
||||
/// other serializable/deserializable objects.
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ObjectBase {
|
||||
// #[serde(rename = "@context")]
|
||||
// #[serde(deserialize_with = "expand_partial")]
|
||||
// pub ap_context: APContext,
|
||||
pub name: Option<String>,
|
||||
pub name_map: Option<HashMap<String, String>>,
|
||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
||||
pub attachment: Vec<Attachment>,
|
||||
#[serde(deserialize_with = "expand_partial_into_vec")]
|
||||
pub attributed_to: Vec<Actor>,
|
||||
pub audience: Option<Audience>,
|
||||
pub media_type: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub content_map: Option<HashMap<String, String>>,
|
||||
pub context: Option<String>,
|
||||
pub start_time: Option<String>,
|
||||
pub end_time: Option<String>,
|
||||
pub generator: Option<Actor>,
|
||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
||||
pub icon: Vec<Link>,
|
||||
// omit in_reply_to
|
||||
pub location: Option<Location>,
|
||||
pub preview: Option<Preview>,
|
||||
pub published: Option<String>,
|
||||
pub updated: Option<String>,
|
||||
pub replies: Option<Collection<()>>, // TODO: type
|
||||
pub summary: Option<String>,
|
||||
pub summary_map: Option<HashMap<String, String>>,
|
||||
#[serde(deserialize_with = "expand_partial_into_vec", default = "Vec::new")]
|
||||
pub tag: Vec<Actor>,
|
||||
// omit url for now: need to merge into_vec & expand_partial
|
||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
||||
pub to: Vec<String>,
|
||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
||||
pub bto: Vec<String>,
|
||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
||||
pub cc: Vec<String>,
|
||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
||||
pub bcc: Vec<String>,
|
||||
pub duration: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, LD, Default)]
|
||||
pub struct Object {
|
||||
#[serde(flatten)]
|
||||
pub base: ObjectBase,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<ObjectKind>,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Link {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<ActivityKind>,
|
||||
pub name: Option<String>,
|
||||
pub href: String,
|
||||
#[serde(rename = "hreflang")]
|
||||
pub href_lang: Option<String>,
|
||||
#[serde(rename = "mediaType")]
|
||||
pub media_type: Option<String>,
|
||||
pub height: u32,
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
/// An Activity is a subtype of Object that describes
|
||||
/// some form of action that may happen,
|
||||
/// is currently happening, or has already happened.
|
||||
///
|
||||
/// The Activity type itself serves as an abstract base type
|
||||
/// for all types of activities.
|
||||
/// It is important to note that the Activity type itself
|
||||
/// does not carry any specific semantics about the kind of action being taken.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Activity {
|
||||
#[serde(flatten)]
|
||||
pub base: ObjectBase,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<ActivityKind>,
|
||||
pub id: String,
|
||||
#[serde(deserialize_with = "expand_partial")]
|
||||
pub actor: Option<Actor>,
|
||||
#[serde(deserialize_with = "expand_partial")]
|
||||
pub object: Option<Object>,
|
||||
pub result: Option<APResult>,
|
||||
pub target: Option<Basic>,
|
||||
pub origin: Option<Basic>,
|
||||
pub instrument: Option<Basic>,
|
||||
}
|
||||
|
||||
/// Instances of IntransitiveActivity are a subtype of Activity
|
||||
/// representing intransitive actions.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IntransitiveActivity {
|
||||
#[serde(flatten)]
|
||||
pub base: ObjectBase,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<ActivityKind>,
|
||||
pub id: String,
|
||||
#[serde(deserialize_with = "expand_partial")]
|
||||
pub actor: Option<Actor>,
|
||||
pub result: Option<APResult>,
|
||||
pub target: Option<Basic>,
|
||||
pub origin: Option<Basic>,
|
||||
pub instrument: Option<Basic>,
|
||||
}
|
||||
pub enum ActorType {
|
||||
Application,
|
||||
Group,
|
||||
Organization,
|
||||
Person,
|
||||
Service,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Basic {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<ActivityKind>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, LD, Default)]
|
||||
pub struct Actor {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<ActivityKind>,
|
||||
#[serde(rename = "attributedTo")]
|
||||
pub attributed_to: Option<String>,
|
||||
pub published: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub context: Option<String>,
|
||||
pub conversation: Option<String>,
|
||||
pub url: Option<String>,
|
||||
#[serde(rename = "to")]
|
||||
#[serde(deserialize_with = "into_vec")]
|
||||
pub to_uris: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn test() {
|
||||
let obj = r#"{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"attributedTo": "http://localhost:3001/u/test",
|
||||
"content": "honk donk",
|
||||
"context": "data:,electrichonkytonk-2jqQ42HyJXctnBKTy1",
|
||||
"conversation": "data:,electrichonkytonk-2jqQ42HyJXctnBKTy1",
|
||||
"id": "htts://localhost:3001/u/test/h/6Q8BFF8W6PZT2ddngZ",
|
||||
"published": "2022-09-30T19:04:45Z",
|
||||
"summary": "",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"url": "https://localhost/u/test/h/6Q8BFF8W6PZT2ddngZ"
|
||||
}"#;
|
||||
|
||||
let obj = serde_json::from_str::<Object>(obj).unwrap();
|
||||
println!();
|
||||
println!("{:#?}", obj);
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
use std::{future::Future, string::FromUtf8Error};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::{Method, StatusCode};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::serde_ext::LDObject;
|
||||
|
||||
const LD_CONTENT_TYPE: &str = "application/ld+json";
|
||||
|
||||
#[async_trait]
|
||||
pub(super) trait Resolvable: Sized {
|
||||
async fn resolve(&self, resolver: &Resolver) -> Result<Self, ResolveError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> Resolvable for T
|
||||
where
|
||||
T: LDObject + for<'de> Deserialize<'de> + Sync,
|
||||
{
|
||||
async fn resolve(&self, resolver: &Resolver) -> Result<Self, ResolveError> {
|
||||
resolver.resolve_into(self.get_iri()).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ResolveError(pub String);
|
||||
|
||||
impl From<reqwest::Error> for ResolveError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
Self(err.to_string())
|
||||
}
|
||||
}
|
||||
impl From<FromUtf8Error> for ResolveError {
|
||||
fn from(e: FromUtf8Error) -> Self {
|
||||
Self(format!(
|
||||
"invalid schema format (tried utf8): {}",
|
||||
e.to_string()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Resolver {
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: reqwest::ClientBuilder::new().build().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get<Out, F, Fut>(&self, iri: String, ok: F) -> Result<Out, ResolveError>
|
||||
where
|
||||
F: FnOnce(reqwest::Response) -> Fut,
|
||||
Fut: Future<Output = Result<Out, ResolveError>>,
|
||||
{
|
||||
let resp = self
|
||||
.client
|
||||
.request(Method::GET, iri)
|
||||
.header("Accept", LD_CONTENT_TYPE)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match resp.status() {
|
||||
StatusCode::OK => Ok(ok(resp).await?),
|
||||
status => Err(ResolveError(format!("non-ok status: {}", status))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_into<'a, T>(&self, iri: String) -> Result<T, ResolveError>
|
||||
where
|
||||
T: for<'de> serde::Deserialize<'de>,
|
||||
{
|
||||
Ok(self
|
||||
.get(iri, |resp| async { Ok(resp.json().await?) })
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn resolve(&self, iri: String) -> Result<Vec<u8>, ResolveError> {
|
||||
Ok(self
|
||||
.get(iri, |resp| async { Ok(resp.bytes().await?.to_vec()) })
|
||||
.await?)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
use serde::{
|
||||
de::{self, MapAccess, Visitor},
|
||||
Deserialize, Deserializer,
|
||||
};
|
||||
|
||||
pub trait LDObject {
|
||||
fn from_iri(iri: &str) -> Self;
|
||||
fn get_iri(&self) -> String;
|
||||
}
|
||||
|
||||
impl<T> LDObject for Option<T>
|
||||
where
|
||||
T: LDObject,
|
||||
{
|
||||
fn from_iri(iri: &str) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_iri(&self) -> String {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
// Allows a value that's a string to be expanded into an object (move string into id property via From<IRI>)
|
||||
// AND the serialization of that object itself
|
||||
pub fn expand_partial<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: Deserialize<'de> + LDObject,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct BaseExpander<T>(PhantomData<fn() -> T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for BaseExpander<T>
|
||||
where
|
||||
T: Deserialize<'de> + LDObject,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("string or map")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<T, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(LDObject::from_iri(value))
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, map: M) -> Result<T, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(BaseExpander(PhantomData))
|
||||
}
|
||||
|
||||
pub(super) fn expand_partial_into_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de> + LDObject,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct BaseExpander<T>(PhantomData<fn() -> T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for BaseExpander<T>
|
||||
where
|
||||
T: Deserialize<'de> + LDObject,
|
||||
{
|
||||
type Value = Vec<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("string or map")
|
||||
}
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(vec![LDObject::from_iri(value)])
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(|v| vec![v])
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(BaseExpander(PhantomData))
|
||||
}
|
||||
|
||||
// Allows deserialization of a single item into a vector of that item
|
||||
// As long as they implement the From<String> trait
|
||||
pub(super) fn into_vec<'de, D, Out>(deserializer: D) -> Result<Vec<Out>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
Out: serde::Deserialize<'de>,
|
||||
{
|
||||
struct VecVisitor<Out>(PhantomData<Vec<Out>>);
|
||||
|
||||
impl<'de, Out> Visitor<'de> for VecVisitor<Out>
|
||||
where
|
||||
Out: serde::Deserialize<'de>,
|
||||
{
|
||||
type Value = Vec<Out>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
let name = std::any::type_name::<Out>();
|
||||
formatter.write_str(format!("{} or Vec<{}>", name, name).as_str())
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Deserialize::deserialize(de::value::StrDeserializer::new(value)).map(|v| vec![v])
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(|v| vec![v])
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(VecVisitor(PhantomData))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use flabk_derive::LD;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::expand_partial;
|
||||
|
||||
#[test]
|
||||
fn expand_partial_populates_iri_from_string() {
|
||||
#[derive(Deserialize, LD, Default)]
|
||||
struct Context {
|
||||
pub id: String,
|
||||
pub context: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WithContext {
|
||||
#[serde(deserialize_with = "expand_partial")]
|
||||
pub context: Context,
|
||||
}
|
||||
|
||||
const JSONLD_INPUT: &str = r#"{"context": "https://www.w3.org/ns/activitystreams"}"#;
|
||||
let result =
|
||||
serde_json::from_str::<WithContext>(JSONLD_INPUT).expect("deserializing with expand");
|
||||
|
||||
assert!(result.context.id == "https://www.w3.org/ns/activitystreams");
|
||||
assert!(result.context.context == false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_partial_expands_into_object_fully() {
|
||||
#[derive(Deserialize, LD, Default)]
|
||||
struct Expanded {
|
||||
pub id: String,
|
||||
pub truth: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Expandable {
|
||||
#[serde(deserialize_with = "expand_partial")]
|
||||
pub expansive: Expanded,
|
||||
}
|
||||
|
||||
const JSONLD_INPUT: &str = r#"{"expansive": { "id": "1", "truth": true }}"#;
|
||||
let result =
|
||||
serde_json::from_str::<Expandable>(JSONLD_INPUT).expect("deserializing with expand");
|
||||
|
||||
assert!(result.expansive.id == "1");
|
||||
assert!(result.expansive.truth);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use super::{keys::Keys, users::Users};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_postgres::{Client, NoTls};
|
||||
|
||||
const DBERR_UNIQUE: &str = "23505";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DB {
|
||||
client: Arc<Mutex<Client>>,
|
||||
}
|
||||
|
||||
impl DB {
|
||||
pub async fn new(host: String, user: String, database: String) -> Result<Self, anyhow::Error> {
|
||||
let (cl, conn) = tokio_postgres::connect(
|
||||
format!("host={host} user={user} dbname={database}").as_str(),
|
||||
NoTls,
|
||||
)
|
||||
.await?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = conn.await {
|
||||
eprintln!("connection error: {}", e);
|
||||
}
|
||||
});
|
||||
let client = Arc::new(Mutex::new(cl));
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
pub fn users(&self) -> Users {
|
||||
Users::new(self.client.clone())
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> Keys {
|
||||
Keys::new(self.client.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DBError {
|
||||
Duplicate,
|
||||
NotFound,
|
||||
Other(tokio_postgres::Error),
|
||||
}
|
||||
|
||||
impl From<tokio_postgres::Error> for DBError {
|
||||
fn from(err: tokio_postgres::Error) -> Self {
|
||||
if let Some(code) = err.code() && code.code() == DBERR_UNIQUE {
|
||||
return DBError::Duplicate;
|
||||
}
|
||||
DBError::Other(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_postgres::Client;
|
||||
|
||||
use super::db::DBError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Keys(Arc<Mutex<Client>>);
|
||||
impl Keys {
|
||||
pub fn new(client: Arc<Mutex<Client>>) -> Self {
|
||||
Self(client)
|
||||
}
|
||||
|
||||
pub async fn get_key(&self, key: &str) -> Result<String, DBError> {
|
||||
Ok(self
|
||||
.0
|
||||
.lock()
|
||||
.await
|
||||
.query("select value from keys where key = $1", &[&key])
|
||||
.await?
|
||||
.first()
|
||||
.ok_or_else(|| DBError::NotFound)?
|
||||
.get("value"))
|
||||
}
|
||||
|
||||
pub async fn set_key(&self, key: &str, value: &str) -> Result<(), DBError> {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.execute(
|
||||
"insert into keys (key, value) values ($1, $2)",
|
||||
&[&key, &value],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod db;
|
||||
pub mod keys;
|
||||
pub mod users;
|