use quarb::{AstAdapter, NodeId, Value};
use serde_json::Value as Json;
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum FirebaseError {
#[error("firebase: {0}")]
Http(#[from] Box<ureq::Error>),
#[error("firebase: {0}")]
Api(String),
#[error("firebase target: {0} (expected firebase://HOST/BASE/PATH[?QUERY])")]
Target(String),
}
enum Kind {
Scalar(Value),
Container(Vec<NodeId>),
Opaque,
}
struct Node {
path: String,
name: Option<String>,
parent: Option<NodeId>,
kind: RefCell<Option<Kind>>,
}
enum GetError {
Status(u16),
Other(String),
}
impl std::fmt::Display for GetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GetError::Status(code) => write!(f, "status code {code}"),
GetError::Other(msg) => f.write_str(msg),
}
}
}
pub type Refs = std::collections::HashMap<String, String>;
pub fn parse_refs(text: &str) -> Result<Refs, FirebaseError> {
let json: Json = serde_json::from_str(text)
.map_err(|e| FirebaseError::Api(format!("refs document: {e}")))?;
let map = json
.get("refs")
.and_then(|v| v.as_object())
.ok_or_else(|| {
FirebaseError::Api(
"refs document: expected {\"refs\": {\"field\": \"container\"}}".into(),
)
})?;
map.iter()
.map(|(k, v)| {
v.as_str()
.map(|t| (k.clone(), t.to_string()))
.ok_or_else(|| {
FirebaseError::Api(format!("refs document: '{k}' target must be a string"))
})
})
.collect()
}
pub struct FirebaseAdapter {
base: String,
query: String,
nodes: RefCell<Vec<Node>>,
by_path: RefCell<HashMap<String, NodeId>>,
refs: Refs,
}
impl FirebaseAdapter {
pub fn connect(target: &str) -> Result<Self, FirebaseError> {
Self::connect_with_refs(target, Refs::new())
}
pub fn connect_with_refs(target: &str, refs: Refs) -> Result<Self, FirebaseError> {
let rest = target
.strip_prefix("firebase://")
.ok_or_else(|| FirebaseError::Target(target.to_string()))?;
let (path, query) = match rest.split_once('?') {
Some((p, q)) => (p, q.to_string()),
None => (rest, String::new()),
};
if path.is_empty() {
return Err(FirebaseError::Target(target.to_string()));
}
let adapter = FirebaseAdapter {
base: format!("https://{}", path.trim_end_matches('/')),
query,
nodes: RefCell::new(vec![Node {
path: String::new(),
name: None,
parent: None,
kind: RefCell::new(None),
}]),
by_path: RefCell::new(HashMap::new()),
refs,
};
adapter
.fetch(NodeId(0))
.map_err(|e| FirebaseError::Api(format!("probing the database root: {e}")))?;
Ok(adapter)
}
pub fn locator(&self, node: NodeId) -> String {
let path = &self.nodes.borrow()[node.0 as usize].path;
if path.is_empty() {
"/".to_string()
} else {
format!("/{path}")
}
}
fn url(&self, path: &str, shallow: bool) -> String {
let mut url = if path.is_empty() {
format!("{}.json", self.base)
} else {
format!("{}/{path}.json", self.base)
};
let mut params = Vec::new();
if shallow {
params.push("shallow=true".to_string());
}
if !self.query.is_empty() {
params.push(self.query.clone());
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
url
}
fn get(&self, url: &str) -> Result<Json, GetError> {
let resp = match ureq::get(url).call() {
Ok(resp) => resp,
Err(ureq::Error::Status(code, _)) => return Err(GetError::Status(code)),
Err(ureq::Error::Transport(t)) => {
let mut msg = t.kind().to_string();
if let Some(detail) = t.message() {
msg.push_str(": ");
msg.push_str(detail);
}
return Err(GetError::Other(msg));
}
};
resp.into_json()
.map_err(|e| GetError::Other(format!("decoding response: {e}")))
}
fn intern(&self, parent: NodeId, key: &str) -> NodeId {
let path = {
let nodes = self.nodes.borrow();
let ppath = &nodes[parent.0 as usize].path;
if ppath.is_empty() {
key.to_string()
} else {
format!("{ppath}/{key}")
}
};
if let Some(&id) = self.by_path.borrow().get(&path) {
return id;
}
let mut nodes = self.nodes.borrow_mut();
let id = NodeId(nodes.len() as u64);
nodes.push(Node {
path: path.clone(),
name: Some(key.to_string()),
parent: Some(parent),
kind: RefCell::new(None),
});
self.by_path.borrow_mut().insert(path, id);
id
}
fn fetch(&self, node: NodeId) -> Result<(), String> {
let (path, fetched) = {
let nodes = self.nodes.borrow();
let n = &nodes[node.0 as usize];
(n.path.clone(), n.kind.borrow().is_some())
};
if fetched {
return Ok(());
}
let json = match self.get(&self.url(&path, true)) {
Ok(j) => j,
Err(GetError::Status(401)) => {
*self.nodes.borrow()[node.0 as usize].kind.borrow_mut() = Some(Kind::Opaque);
return Ok(());
}
Err(e) => return Err(e.to_string()),
};
let kind = match &json {
Json::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_by(|a, b| match (a.parse::<i64>(), b.parse::<i64>()) {
(Ok(x), Ok(y)) => x.cmp(&y),
(Ok(_), Err(_)) => std::cmp::Ordering::Less,
(Err(_), Ok(_)) => std::cmp::Ordering::Greater,
(Err(_), Err(_)) => a.cmp(b),
});
let children = keys.iter().map(|k| self.intern(node, k)).collect();
Kind::Container(children)
}
Json::Array(items) => {
let children = (0..items.len())
.map(|i| self.intern(node, &i.to_string()))
.collect();
Kind::Container(children)
}
other => Kind::Scalar(scalar_of(other)),
};
*self.nodes.borrow()[node.0 as usize].kind.borrow_mut() = Some(kind);
Ok(())
}
fn touched(&self, node: NodeId) {
if let Err(e) = self.fetch(node) {
let path = self.locator(node);
eprintln!("quarb-firebase: fetching {path}: {e}");
*self.nodes.borrow()[node.0 as usize].kind.borrow_mut() =
Some(Kind::Container(Vec::new()));
}
}
fn field(&self, node: NodeId, name: &str) -> Option<Value> {
let child = self.intern(node, name);
self.touched(child);
let nodes = self.nodes.borrow();
match &*nodes[child.0 as usize].kind.borrow() {
Some(Kind::Scalar(v)) => match v {
Value::Null => None,
other => Some(other.clone()),
},
_ => None,
}
}
}
fn scalar_of(value: &Json) -> Value {
match value {
Json::Bool(b) => Value::Bool(*b),
Json::Number(n) => n
.as_i64()
.map(Value::Int)
.or_else(|| n.as_f64().map(Value::Float))
.unwrap_or(Value::Null),
Json::String(s) => Value::Str(s.clone()),
_ => Value::Null,
}
}
impl AstAdapter for FirebaseAdapter {
fn root(&self) -> NodeId {
NodeId(0)
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.touched(node);
let nodes = self.nodes.borrow();
match &*nodes[node.0 as usize].kind.borrow() {
Some(Kind::Container(c)) => c.clone(),
_ => Vec::new(),
}
}
fn name(&self, node: NodeId) -> Option<String> {
self.nodes.borrow()[node.0 as usize].name.clone()
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.nodes.borrow()[node.0 as usize].parent
}
fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
self.touched(node);
{
let nodes = self.nodes.borrow();
match &*nodes[node.0 as usize].kind.borrow() {
Some(Kind::Scalar(_)) => return Vec::new(),
Some(Kind::Container(c)) => {
return c
.iter()
.copied()
.filter(|&c| nodes[c.0 as usize].name.as_deref() == Some(name))
.collect();
}
_ => {}
}
}
let child = self.intern(node, name);
self.touched(child);
let nodes = self.nodes.borrow();
match &*nodes[child.0 as usize].kind.borrow() {
Some(Kind::Scalar(Value::Null)) | None => Vec::new(),
_ => vec![child],
}
}
fn traits(&self, node: NodeId) -> Vec<String> {
self.touched(node);
let nodes = self.nodes.borrow();
let t = match &*nodes[node.0 as usize].kind.borrow() {
Some(Kind::Container(_) | Kind::Opaque) => "object",
Some(Kind::Scalar(Value::Str(_))) => "string",
Some(Kind::Scalar(Value::Int(_) | Value::Float(_))) => "number",
Some(Kind::Scalar(Value::Bool(_))) => "boolean",
_ => "null",
};
vec![t.to_string()]
}
fn property(&self, node: NodeId, name: &str) -> Option<Value> {
self.field(node, name)
}
fn default_value(&self, node: NodeId) -> Option<Value> {
self.touched(node);
let nodes = self.nodes.borrow();
match &*nodes[node.0 as usize].kind.borrow() {
Some(Kind::Scalar(v)) => Some(v.clone()),
_ => None,
}
}
fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
match key {
"path" => Some(Value::Str(self.locator(node))),
"type" => Some(Value::Str(self.traits(node).remove(0))),
"length" => {
self.touched(node);
let nodes = self.nodes.borrow();
match &*nodes[node.0 as usize].kind.borrow() {
Some(Kind::Container(c)) => Some(Value::Int(c.len() as i64)),
Some(Kind::Scalar(Value::Str(s))) => Some(Value::Int(s.chars().count() as i64)),
_ => None,
}
}
_ => None,
}
}
fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
let container = hint.or_else(|| self.refs.get(property).map(String::as_str))?;
let value = self.field(node, property)?;
let root_child = self.intern(NodeId(0), container);
let target = self.intern(root_child, &value.to_string());
self.touched(target);
let nodes = self.nodes.borrow();
match &*nodes[target.0 as usize].kind.borrow() {
Some(Kind::Scalar(Value::Null)) | None => None,
_ => Some(target),
}
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
let mut declared: Vec<(&String, &String)> = self.refs.iter().collect();
declared.sort();
let mut out = Vec::new();
for (field, target) in declared {
let root_child = self.intern(NodeId(0), target);
if let Some(elem_field) = field.strip_suffix("/*") {
let container = self.intern(node, elem_field);
for elem in self.children(container) {
let Some(v) = self.default_value(elem) else {
continue;
};
let t = self.intern(root_child, &v.to_string());
self.touched(t);
let nodes = self.nodes.borrow();
if !matches!(
&*nodes[t.0 as usize].kind.borrow(),
Some(Kind::Scalar(Value::Null)) | None
) {
out.push((elem_field.to_string(), t));
}
}
} else if let Some(v) = self.field(node, field) {
let t = self.intern(root_child, &v.to_string());
self.touched(t);
let nodes = self.nodes.borrow();
if !matches!(
&*nodes[t.0 as usize].kind.borrow(),
Some(Kind::Scalar(Value::Null)) | None
) {
out.push((field.clone(), t));
}
}
}
out
}
}