phemios_core/
federation.rs1use serde_json::Value;
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum Source {
6 Local,
8 Server,
10}
11impl Source {
12 pub fn parse(value: &str) -> Result<Self, String> {
14 match value {
15 "local" => Ok(Self::Local),
16 "server" => Ok(Self::Server),
17 _ => Err("Origine de bibliothèque invalide.".into()),
18 }
19 }
20 pub fn name(self) -> &'static str {
22 match self {
23 Self::Local => "local",
24 Self::Server => "server",
25 }
26 }
27}
28fn entity_kind(kind: &str) -> bool {
29 matches!(
30 kind,
31 "artist" | "release" | "track" | "recording" | "work" | "release_group" | "label" | "genre"
32 )
33}
34pub fn qualify(reference: &str, source: Source) -> String {
36 match reference.split_once(':') {
37 Some((kind, suffix)) if entity_kind(kind) => {
38 format!("{kind}:@phemios/{}/{suffix}", source.name())
39 }
40 _ => reference.into(),
41 }
42}
43pub fn resolve(reference: &str) -> Result<(Source, String), String> {
46 let Some((kind, key)) = reference.split_once(':') else {
47 return Ok((Source::Server, reference.into()));
48 };
49 let Some(rest) = key.strip_prefix("@phemios/") else {
50 return Ok((Source::Server, reference.into()));
51 };
52 let (source, suffix) = rest
53 .split_once('/')
54 .ok_or("Référence de bibliothèque invalide.")?;
55 if suffix.is_empty() {
56 return Err("Référence de bibliothèque invalide.".into());
57 }
58 Ok((Source::parse(source)?, format!("{kind}:{suffix}")))
59}
60fn identity_key(key: &str) -> bool {
61 matches!(
62 key,
63 "items"
64 | "reference"
65 | "ref"
66 | "release"
67 | "track"
68 | "artist"
69 | "recording"
70 | "work"
71 | "label"
72 | "labels"
73 | "release_refs"
74 | "contributors"
75 | "tracks"
76 | "albums"
77 | "releases"
78 | "artists"
79 )
80}
81pub fn map(value: &mut Value, source: Source, incoming: bool) -> Result<(), String> {
83 fn walk(value: &mut Value, key: &str, source: Source, incoming: bool) -> Result<(), String> {
84 match value {
85 Value::Object(fields) => {
86 for (key, value) in fields {
87 walk(value, key, source, incoming)?;
88 }
89 }
90 Value::Array(rows) => {
91 for row in rows {
92 walk(row, key, source, incoming)?;
93 }
94 }
95 Value::String(text)
96 if identity_key(key)
97 && text
98 .split_once(':')
99 .is_some_and(|(kind, _)| entity_kind(kind)) =>
100 {
101 if incoming {
102 *text = qualify(text, source);
103 } else {
104 let (owner, raw) = resolve(text)?;
105 if owner != source {
106 return Err("Cette opération doit concerner une seule bibliothèque.".into());
107 }
108 *text = raw;
109 }
110 }
111 _ => {}
112 }
113 Ok(())
114 }
115 walk(value, "", source, incoming)
116}
117#[cfg(test)]
118#[path = "federation_tests.rs"]
119mod tests;
120
121pub fn request_origin(
124 query: &mut std::collections::BTreeMap<String, String>,
125 body: &mut Option<Value>,
126 fallback: Source,
127) -> Result<Source, String> {
128 let hint = ["ref", "reference", "track"]
129 .iter()
130 .find_map(|key| query.get(*key).map(String::as_str))
131 .or_else(|| {
132 body.as_ref().and_then(|value| {
133 ["release", "reference", "track"]
134 .iter()
135 .find_map(|key| value[*key].as_str())
136 })
137 });
138 let origin = hint
139 .map(resolve)
140 .transpose()?
141 .map_or(fallback, |value| value.0);
142 for key in ["ref", "reference", "track"] {
143 if let Some(reference) = query.get_mut(key) {
144 let (owner, raw) = resolve(reference)?;
145 if owner != origin {
146 return Err("Origines incompatibles.".into());
147 }
148 *reference = raw;
149 }
150 }
151 if let Some(value) = body {
152 map(value, origin, false)?;
153 }
154 Ok(origin)
155}