use serde_json::{json, Value};
use std::collections::{HashMap, HashSet};
use thiserror::Error;
use super::framing_embed::{apply_defaults, apply_explicit, embed_subject, EmbedDecision};
use super::framing_match::matches_frame;
#[derive(Debug, Error)]
pub enum FramingError {
#[error("Invalid frame: {0}")]
InvalidFrame(String),
#[error("Expansion required before framing: {0}")]
ExpansionRequired(String),
#[error("Invalid @embed value: {0}")]
InvalidEmbedValue(String),
#[error("Cyclic embed detected for subject: {0}")]
CyclicEmbed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmbedPolicy {
First,
Last,
Always,
Never,
Link,
}
impl Default for EmbedPolicy {
fn default() -> Self {
Self::First
}
}
impl EmbedPolicy {
pub fn parse_embed_value(s: &str) -> Result<Self, FramingError> {
match s {
"@first" | "@once" => Ok(Self::First),
"@last" => Ok(Self::Last),
"@always" => Ok(Self::Always),
"@never" => Ok(Self::Never),
"@link" => Ok(Self::Link),
other => Err(FramingError::InvalidEmbedValue(other.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct FramingOptions {
pub embed: EmbedPolicy,
pub explicit: bool,
pub omit_default: bool,
pub require_all: bool,
pub pruned_none: bool,
}
impl Default for FramingOptions {
fn default() -> Self {
Self {
embed: EmbedPolicy::First,
explicit: false,
omit_default: false,
require_all: false,
pruned_none: false,
}
}
}
#[derive(Debug, Default)]
pub struct FramingState {
pub embedded: HashSet<String>,
pub link: HashMap<String, Value>,
}
impl FramingState {
pub fn new() -> Self {
Self {
embedded: HashSet::new(),
link: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct JsonLdFramer {
options: FramingOptions,
}
impl JsonLdFramer {
pub fn new(options: FramingOptions) -> Self {
Self { options }
}
pub fn frame(&self, input: &Value, frame: &Value) -> Result<Value, FramingError> {
let subjects = self.build_subject_map(input)?;
let options = self.resolve_frame_options(frame);
let mut state = FramingState::new();
let framed = self.frame_subjects(&mut state, &subjects, frame, &options)?;
Ok(json!({
"@context": {},
"@graph": framed
}))
}
fn build_subject_map(&self, input: &Value) -> Result<HashMap<String, Value>, FramingError> {
let mut map: HashMap<String, Value> = HashMap::new();
let nodes = match input {
Value::Array(arr) => arr.as_slice(),
Value::Object(_) => std::slice::from_ref(input),
Value::Null => return Ok(map),
_ => {
return Err(FramingError::ExpansionRequired(
"Input must be an expanded JSON-LD array or object".to_string(),
))
}
};
for node in nodes {
self.collect_subjects(node, &mut map);
}
Ok(map)
}
fn collect_subjects(&self, node: &Value, map: &mut HashMap<String, Value>) {
let obj = match node {
Value::Object(o) => o,
Value::Array(arr) => {
for item in arr {
self.collect_subjects(item, map);
}
return;
}
_ => return,
};
if let Some(Value::String(id)) = obj.get("@id") {
map.entry(id.clone()).or_insert_with(|| node.clone());
}
for (key, val) in obj {
if key == "@graph" {
self.collect_subjects(val, map);
} else if key.starts_with('@') {
} else {
if let Value::Array(vals) = val {
for v in vals {
if let Value::Object(vo) = v {
if !vo.contains_key("@value") {
self.collect_subjects(v, map);
}
}
}
}
}
}
}
fn resolve_frame_options(&self, frame: &Value) -> FramingOptions {
let mut opts = self.options.clone();
if let Value::Object(fobj) = frame {
if let Some(Value::String(embed_val)) = fobj.get("@embed") {
if let Ok(policy) = EmbedPolicy::parse_embed_value(embed_val.as_str()) {
opts.embed = policy;
}
}
if let Some(Value::Bool(explicit)) = fobj.get("@explicit") {
opts.explicit = *explicit;
}
if let Some(Value::Bool(omit)) = fobj.get("@omitDefault") {
opts.omit_default = *omit;
}
if let Some(Value::Bool(req_all)) = fobj.get("@requireAll") {
opts.require_all = *req_all;
}
}
opts
}
pub fn frame_subjects(
&self,
state: &mut FramingState,
subjects: &HashMap<String, Value>,
frame: &Value,
options: &FramingOptions,
) -> Result<Vec<Value>, FramingError> {
let mut output: Vec<Value> = Vec::new();
let mut ids: Vec<&String> = subjects.keys().collect();
ids.sort();
for id in ids {
let subject = subjects.get(id).expect("key from map");
if !matches_frame(subject, frame, options) {
continue;
}
let embed_decision =
super::framing_embed::apply_embed_policy(state, id, options.embed.clone());
match embed_decision {
EmbedDecision::Skip => {
output.push(json!({"@id": id}));
}
EmbedDecision::Link => {
if let Some(linked) = state.link.get(id) {
output.push(linked.clone());
} else {
output.push(json!({"@id": id}));
}
}
EmbedDecision::Full => {
state.embedded.insert(id.clone());
let mut embedded = embed_subject(state, subject, frame, subjects, options)?;
embedded = apply_explicit(&embedded, frame, options);
apply_defaults(&mut embedded, frame, options);
if options.embed == EmbedPolicy::Link {
state.link.insert(id.clone(), embedded.clone());
}
output.push(embedded);
}
}
}
Ok(output)
}
}