use std::{borrow::Cow, collections::HashMap};
use serde_json::{Map, Value as JsonValue};
use crate::{
backend::types::JsonbValue,
error::{FraiseQLError, Result},
graphql::FieldSelection,
schema::{CompiledSchema, FieldDefinition, FieldType},
utils::casing::{to_camel_case, to_snake_case},
};
#[derive(Debug, Clone)]
pub struct FieldMapping {
pub source: String,
pub output: String,
pub source_fallback: Option<String>,
pub nested_typename: Option<String>,
pub nested_fields: Option<Vec<FieldMapping>>,
pub declared_scalar: bool,
pub localized: Option<Vec<String>>,
}
pub fn stored_key_candidates(field_name: &str) -> (String, Option<String>) {
let snake = to_snake_case(field_name);
let camel = to_camel_case(field_name);
let fallback = (camel != snake).then_some(camel);
(snake, fallback)
}
impl FieldMapping {
#[must_use]
pub fn simple(name: impl Into<String>) -> Self {
let name = name.into();
let (source, source_fallback) = stored_key_candidates(&name);
Self {
source,
output: name,
source_fallback,
nested_typename: None,
nested_fields: None,
declared_scalar: false,
localized: None,
}
}
#[must_use]
pub fn aliased(source: impl Into<String>, alias: impl Into<String>) -> Self {
let (source, source_fallback) = stored_key_candidates(&source.into());
Self {
source,
output: alias.into(),
source_fallback,
nested_typename: None,
nested_fields: None,
declared_scalar: false,
localized: None,
}
}
#[must_use]
pub fn nested_object(
name: impl Into<String>,
typename: impl Into<String>,
fields: Vec<FieldMapping>,
) -> Self {
let name = name.into();
let (source, source_fallback) = stored_key_candidates(&name);
Self {
source,
output: name,
source_fallback,
nested_typename: Some(typename.into()),
nested_fields: Some(fields),
declared_scalar: false,
localized: None,
}
}
#[must_use]
pub fn nested_object_aliased(
source: impl Into<String>,
alias: impl Into<String>,
typename: impl Into<String>,
fields: Vec<FieldMapping>,
) -> Self {
let (source, source_fallback) = stored_key_candidates(&source.into());
Self {
source,
output: alias.into(),
source_fallback,
nested_typename: Some(typename.into()),
nested_fields: Some(fields),
declared_scalar: false,
localized: None,
}
}
#[must_use]
pub fn with_nested_typename(mut self, typename: impl Into<String>) -> Self {
self.nested_typename = Some(typename.into());
self
}
#[must_use]
pub fn with_nested_fields(mut self, fields: Vec<FieldMapping>) -> Self {
self.nested_fields = Some(fields);
self
}
#[must_use]
pub fn lookup_in<'a>(
&self,
map: &'a serde_json::Map<String, JsonValue>,
) -> Option<&'a JsonValue> {
map.get(&self.source)
.or_else(|| self.source_fallback.as_ref().and_then(|fb| map.get(fb)))
}
fn take_from(&self, map: &mut Map<String, JsonValue>) -> Option<(String, JsonValue)> {
map.remove_entry(&self.source)
.or_else(|| self.source_fallback.as_ref().and_then(|fb| map.remove_entry(fb)))
}
fn stored_keys(&self) -> impl Iterator<Item = &str> {
std::iter::once(self.source.as_str()).chain(self.source_fallback.as_deref())
}
}
fn later_readers(fields: &[FieldMapping]) -> Vec<bool> {
let mut last_reader: HashMap<&str, usize> = HashMap::with_capacity(fields.len() * 2);
for (index, field) in fields.iter().enumerate() {
for key in field.stored_keys() {
last_reader.insert(key, index);
}
}
fields
.iter()
.enumerate()
.map(|(index, field)| field.stored_keys().any(|key| last_reader[key] > index))
.collect()
}
enum Shape<'a> {
Object(Cow<'a, Map<String, JsonValue>>),
Array(Cow<'a, [JsonValue]>),
Other(Cow<'a, JsonValue>),
}
impl<'a> Shape<'a> {
fn of(value: Cow<'a, JsonValue>) -> Self {
match value {
Cow::Borrowed(JsonValue::Object(map)) => Self::Object(Cow::Borrowed(map)),
Cow::Owned(JsonValue::Object(map)) => Self::Object(Cow::Owned(map)),
Cow::Borrowed(JsonValue::Array(items)) => Self::Array(Cow::Borrowed(items.as_slice())),
Cow::Owned(JsonValue::Array(items)) => Self::Array(Cow::Owned(items)),
other => Self::Other(other),
}
}
}
fn items_of(items: Cow<'_, [JsonValue]>) -> Box<dyn Iterator<Item = Cow<'_, JsonValue>> + '_> {
match items {
Cow::Borrowed(items) => Box::new(items.iter().map(Cow::Borrowed)),
Cow::Owned(items) => Box::new(items.into_iter().map(Cow::Owned)),
}
}
#[derive(Debug, Clone)]
pub struct ProjectionMapper {
pub fields: Vec<FieldMapping>,
pub typename: Option<String>,
pub federation_mode: bool,
}
const fn excludes_json_composites(field_type: &FieldType) -> bool {
matches!(
field_type,
FieldType::String
| FieldType::Int
| FieldType::Float
| FieldType::Boolean
| FieldType::Id
| FieldType::DateTime
| FieldType::Date
| FieldType::Time
| FieldType::Uuid
| FieldType::Decimal
)
}
impl ProjectionMapper {
#[must_use]
pub fn new(fields: Vec<String>) -> Self {
Self {
fields: fields.into_iter().map(FieldMapping::simple).collect(),
typename: None,
federation_mode: false,
}
}
#[must_use]
pub const fn with_mappings(fields: Vec<FieldMapping>) -> Self {
Self {
fields,
typename: None,
federation_mode: false,
}
}
#[must_use]
pub fn with_declared_scalars(mut self, schema: &CompiledSchema, entity_type: &str) -> Self {
let Some(type_def) = schema.find_type(entity_type) else {
return self;
};
for mapping in &mut self.fields {
let declared = type_def
.fields
.iter()
.find(|f| f.name == mapping.output || f.name == mapping.source);
if let Some(fd) = declared {
mapping.declared_scalar = excludes_json_composites(&fd.field_type);
if fd.localized {
mapping.localized = crate::runtime::localization_chain(schema);
}
}
}
self
}
#[must_use]
pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
self.typename = Some(typename.into());
self
}
#[must_use]
pub const fn with_federation_mode(mut self, enabled: bool) -> Self {
self.federation_mode = enabled;
self
}
pub fn project(&self, jsonb: &JsonbValue) -> Result<JsonValue> {
self.project_row(Cow::Borrowed(jsonb.as_value()), &[])
}
fn project_row(&self, row: Cow<'_, JsonValue>, later_readers: &[bool]) -> Result<JsonValue> {
match Shape::of(row) {
Shape::Object(map) => {
self.project_object(map, self.typename.as_deref(), &self.fields, later_readers)
},
Shape::Array(items) => items_of(items)
.map(|item| match Shape::of(item) {
Shape::Object(obj) => self.project_object(
obj,
self.typename.as_deref(),
&self.fields,
later_readers,
),
Shape::Array(items) => Ok(JsonValue::Array(items.into_owned())),
Shape::Other(other) => Ok(other.into_owned()),
})
.collect::<Result<Vec<_>>>()
.map(JsonValue::Array),
Shape::Other(other) => Ok(other.into_owned()),
}
}
pub fn project_json_object(
&self,
map: &serde_json::Map<String, JsonValue>,
) -> Result<JsonValue> {
self.project_object(Cow::Borrowed(map), self.typename.as_deref(), &self.fields, &[])
}
fn project_object(
&self,
source: Cow<'_, Map<String, JsonValue>>,
typename: Option<&str>,
fields: &[FieldMapping],
later_readers: &[bool],
) -> Result<JsonValue> {
let mut result = Map::new();
if let Some(typename) = typename {
result.insert("__typename".to_string(), JsonValue::String(typename.to_string()));
}
let mut source = source;
for (index, field) in fields.iter().enumerate() {
let found = match &mut source {
Cow::Borrowed(map) => {
let map: &Map<String, JsonValue> = map;
field.lookup_in(map).map(|value| (None, Cow::Borrowed(value)))
},
Cow::Owned(map) => {
let taken = if later_readers.get(index).copied().unwrap_or(true) {
field.lookup_in(map).map(|value| (None, value.clone()))
} else {
field.take_from(map).map(|(key, value)| (Some(key), value))
};
taken.map(|(key, value)| (key, Cow::Owned(value)))
},
};
let Some((key, value)) = found else {
result.insert(field.output.clone(), JsonValue::Null);
continue;
};
let output =
key.filter(|key| *key == field.output).unwrap_or_else(|| field.output.clone());
result.insert(output, self.project_nested_value(value, field)?);
}
Ok(JsonValue::Object(result))
}
#[allow(clippy::self_only_used_in_recursion)] fn project_nested_value(
&self,
value: Cow<'_, JsonValue>,
field: &FieldMapping,
) -> Result<JsonValue> {
if let Some(chain) = &field.localized {
return Ok(crate::runtime::localize(&value, chain));
}
match Shape::of(value) {
Shape::Object(obj) => {
let Some(typename) = field.nested_typename.as_deref() else {
return Ok(JsonValue::Object(obj.into_owned()));
};
if let Some(nested_fields) = &field.nested_fields {
let later = match obj {
Cow::Owned(_) => later_readers(nested_fields),
Cow::Borrowed(_) => Vec::new(),
};
return self.project_object(obj, Some(typename), nested_fields, &later);
}
let mut result = Map::new();
result.insert("__typename".to_string(), JsonValue::String(typename.to_string()));
result.extend(obj.into_owned());
Ok(JsonValue::Object(result))
},
Shape::Array(items) => {
if field.nested_typename.is_some() {
items_of(items)
.map(|item| self.project_nested_value(item, field))
.collect::<Result<Vec<_>>>()
.map(JsonValue::Array)
} else {
Ok(JsonValue::Array(items.into_owned()))
}
},
Shape::Other(value) => {
if field.declared_scalar {
return Ok(value.into_owned());
}
if let JsonValue::String(ref s) = *value {
if let Ok(parsed @ (JsonValue::Object(_) | JsonValue::Array(_))) =
serde_json::from_str::<JsonValue>(s)
{
return self.project_nested_value(Cow::Owned(parsed), field);
}
}
Ok(value.into_owned())
},
}
}
}
pub struct ResultProjector {
mapper: ProjectionMapper,
}
impl ResultProjector {
#[must_use]
pub fn new(fields: Vec<String>) -> Self {
Self {
mapper: ProjectionMapper::new(fields),
}
}
#[must_use]
pub const fn with_mappings(fields: Vec<FieldMapping>) -> Self {
Self {
mapper: ProjectionMapper::with_mappings(fields),
}
}
#[must_use]
pub fn with_declared_scalars(mut self, schema: &CompiledSchema, entity_type: &str) -> Self {
self.mapper = self.mapper.with_declared_scalars(schema, entity_type);
self
}
#[must_use]
pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
self.mapper = self.mapper.with_typename(typename);
self
}
#[must_use]
pub fn configure_typename_from_selections(
self,
selections: &[FieldSelection],
entity_type: &str,
) -> Self {
let wants_typename = selections
.first()
.is_some_and(|root| root.nested_fields.iter().any(|f| f.name == "__typename"));
if wants_typename {
self.with_typename(entity_type)
} else {
self
}
}
#[must_use]
pub fn with_federation_mode(mut self, enabled: bool) -> Self {
self.mapper = self.mapper.with_federation_mode(enabled);
self
}
pub fn project_results(&self, results: &[JsonbValue], is_list: bool) -> Result<JsonValue> {
if is_list {
let projected: Result<Vec<JsonValue>> =
results.iter().map(|r| self.mapper.project(r)).collect();
Ok(JsonValue::Array(projected?))
} else {
if let Some(first) = results.first() {
self.mapper.project(first)
} else {
Ok(JsonValue::Null)
}
}
}
pub fn project_results_owned(
&self,
results: std::sync::Arc<Vec<JsonbValue>>,
is_list: bool,
) -> Result<JsonValue> {
let rows = match std::sync::Arc::try_unwrap(results) {
Ok(rows) => rows,
Err(shared) => return self.project_results(&shared, is_list),
};
let later_readers = later_readers(&self.mapper.fields);
let mut rows = rows
.into_iter()
.map(|row| self.mapper.project_row(Cow::Owned(row.into_value()), &later_readers));
if is_list {
rows.collect::<Result<Vec<_>>>().map(JsonValue::Array)
} else {
rows.next().unwrap_or(Ok(JsonValue::Null))
}
}
#[must_use]
pub fn wrap_in_data_envelope(result: JsonValue, query_name: &str) -> JsonValue {
let mut data = Map::new();
data.insert(query_name.to_string(), result);
let mut response = Map::new();
response.insert("data".to_string(), JsonValue::Object(data));
JsonValue::Object(response)
}
pub fn add_typename_only(
&self,
projected_data: &JsonbValue,
typename: &str,
) -> Result<JsonValue> {
let value = projected_data.as_value();
match value {
JsonValue::Object(map) => {
let mut result = map.clone();
result.insert("__typename".to_string(), JsonValue::String(typename.to_string()));
Ok(JsonValue::Object(result))
},
JsonValue::Array(arr) => {
let updated: Result<Vec<JsonValue>> = arr
.iter()
.map(|item| {
if let JsonValue::Object(obj) = item {
let mut result = obj.clone();
result.insert(
"__typename".to_string(),
JsonValue::String(typename.to_string()),
);
Ok(JsonValue::Object(result))
} else {
Ok(item.clone())
}
})
.collect();
Ok(JsonValue::Array(updated?))
},
v => Ok(v.clone()),
}
}
#[must_use]
pub fn wrap_error(error: &FraiseQLError) -> JsonValue {
let mut error_obj = Map::new();
error_obj.insert("message".to_string(), JsonValue::String(error.to_string()));
let mut response = Map::new();
response.insert("errors".to_string(), JsonValue::Array(vec![JsonValue::Object(error_obj)]));
JsonValue::Object(response)
}
}
#[must_use]
pub fn project_entity(
entity: &JsonValue,
type_name: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
) -> JsonValue {
if selections.is_empty() {
return JsonValue::Object(Map::new());
}
project_entity_at(entity, type_name, selections, schema)
}
fn project_entity_at(
entity: &JsonValue,
type_name: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
) -> JsonValue {
let JsonValue::Object(obj) = entity else {
return entity.clone();
};
let type_def = schema.find_type(type_name);
let mut out = Map::new();
for sel in effective_selections(selections, type_name, schema) {
if sel.name == "__typename" {
out.insert(sel.response_key().to_string(), JsonValue::String(type_name.to_string()));
continue;
}
if let Some(base) = type_def.and_then(|td| td.translations_of(&sel.name)) {
let listed = crate::runtime::translations_read(schema, sel).map_or_else(
|| JsonValue::Array(Vec::new()),
|read| {
let stored = lookup_source(obj, base.name.as_str()).unwrap_or(&JsonValue::Null);
crate::runtime::translations(stored, &read)
},
);
out.insert(sel.response_key().to_string(), listed);
continue;
}
let field_def =
type_def.and_then(|td| td.fields.iter().find(|f| f.name.as_str() == sel.name));
let Some(value) = lookup_source(obj, &sel.name) else {
out.insert(sel.response_key().to_string(), JsonValue::Null);
continue;
};
let projected = project_field_value(value, field_def, sel, schema);
out.insert(sel.response_key().to_string(), projected);
}
JsonValue::Object(out)
}
fn project_field_value(
value: &JsonValue,
field_def: Option<&FieldDefinition>,
selection: &FieldSelection,
schema: &CompiledSchema,
) -> JsonValue {
let nested = selection.nested_fields.as_slice();
if let Some(fd) = field_def {
if fd.localized {
return crate::runtime::selection_chain(schema, selection)
.map_or_else(|| value.clone(), |chain| crate::runtime::localize(value, &chain));
}
if !fd.field_type.is_scalar() && !fd.field_type.is_list() {
if let Some(child_type) = fd.field_type.type_name() {
match value {
JsonValue::Object(_) => {
return project_entity_at(value, child_type, nested, schema);
},
JsonValue::String(s) => {
if let Ok(parsed @ JsonValue::Object(_)) =
serde_json::from_str::<JsonValue>(s)
{
return project_entity_at(&parsed, child_type, nested, schema);
}
},
_ => {},
}
}
} else if fd.field_type.is_list() {
if let Some(child_type) = fd.field_type.inner_type().and_then(FieldType::type_name) {
if let JsonValue::Array(arr) = value {
return JsonValue::Array(
arr.iter()
.map(|el| project_list_element(el, child_type, nested, schema))
.collect(),
);
}
if let JsonValue::String(s) = value {
if let Ok(JsonValue::Array(arr)) = serde_json::from_str::<JsonValue>(s) {
return JsonValue::Array(
arr.iter()
.map(|el| project_list_element(el, child_type, nested, schema))
.collect(),
);
}
}
}
}
}
value.clone()
}
fn project_list_element(
element: &JsonValue,
child_type: &str,
nested: &[FieldSelection],
schema: &CompiledSchema,
) -> JsonValue {
match element {
JsonValue::Object(_) => project_entity_at(element, child_type, nested, schema),
JsonValue::String(s) => match serde_json::from_str::<JsonValue>(s) {
Ok(parsed @ JsonValue::Object(_)) => {
project_entity_at(&parsed, child_type, nested, schema)
},
_ => element.clone(),
},
_ => element.clone(),
}
}
pub fn project_nested_lists(
value: &mut JsonValue,
type_name: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
) {
match value {
JsonValue::Array(arr) => {
for el in arr.iter_mut() {
project_nested_lists(el, type_name, selections, schema);
}
},
JsonValue::Object(obj) => {
let type_def = schema.find_type(type_name);
for sel in effective_selections(selections, type_name, schema) {
if sel.name == "__typename" {
continue;
}
let Some(fd) =
type_def.and_then(|td| td.fields.iter().find(|f| f.name.as_str() == sel.name))
else {
continue;
};
if fd.field_type.is_scalar() {
continue;
}
let key = sel.response_key();
if fd.field_type.is_list() {
if let Some(child_type) =
fd.field_type.inner_type().and_then(FieldType::type_name)
{
if let Some(JsonValue::Array(arr)) = obj.get_mut(key) {
for el in arr.iter_mut() {
*el = project_entity(el, child_type, &sel.nested_fields, schema);
}
}
}
} else if let Some(child_type) = fd.field_type.type_name() {
if let Some(child) = obj.get_mut(key) {
project_nested_lists(child, child_type, &sel.nested_fields, schema);
}
}
}
},
_ => {},
}
}
pub fn stamp_nested_typenames(
value: &mut JsonValue,
type_name: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
) {
match value {
JsonValue::Array(arr) => {
for el in arr.iter_mut() {
stamp_nested_typenames(el, type_name, selections, schema);
}
},
JsonValue::Object(obj) => {
let type_def = schema.find_type(type_name);
for sel in effective_selections(selections, type_name, schema) {
if sel.name == "__typename" {
continue;
}
let Some(fd) =
type_def.and_then(|td| td.fields.iter().find(|f| f.name.as_str() == sel.name))
else {
continue;
};
if fd.field_type.is_scalar() || fd.field_type.is_list() {
continue;
}
let Some(child_type) = fd.field_type.type_name() else {
continue;
};
let key = sel.response_key().to_string();
let child_selections = &sel.nested_fields;
let Some(child) = obj.get_mut(&key) else {
continue;
};
if let JsonValue::Object(child_obj) = child {
reinsert_typename_in_order(child_obj, child_selections, child_type, schema);
}
stamp_nested_typenames(child, child_type, child_selections, schema);
}
},
_ => {},
}
}
fn reinsert_typename_in_order(
obj: &mut Map<String, JsonValue>,
selections: &[FieldSelection],
type_name: &str,
schema: &CompiledSchema,
) {
if !effective_selections(selections, type_name, schema)
.iter()
.any(|sel| sel.name == "__typename")
{
return;
}
let mut rebuilt = Map::new();
for sel in effective_selections(selections, type_name, schema) {
let key = sel.response_key();
if sel.name == "__typename" {
rebuilt.insert(key.to_string(), JsonValue::String(type_name.to_string()));
} else if let Some(v) = obj.get(key) {
rebuilt.insert(key.to_string(), v.clone());
}
}
for (k, v) in obj.iter() {
if !rebuilt.contains_key(k) {
rebuilt.insert(k.clone(), v.clone());
}
}
*obj = rebuilt;
}
fn lookup_source<'a>(obj: &'a Map<String, JsonValue>, field_name: &str) -> Option<&'a JsonValue> {
let (snake, camel) = stored_key_candidates(field_name);
obj.get(&snake).or_else(|| camel.and_then(|c| obj.get(&c)))
}
pub fn effective_selections<'a>(
selections: &'a [FieldSelection],
type_name: &str,
schema: &CompiledSchema,
) -> Vec<&'a FieldSelection> {
let mut out = Vec::new();
for sel in selections {
if let Some(frag_type) = sel.name.strip_prefix("...on ") {
let frag_type = frag_type.trim();
let applies = frag_type == type_name
|| schema
.find_type(type_name)
.is_some_and(|td| td.implements.iter().any(|i| i == frag_type));
if applies {
out.extend(effective_selections(&sel.nested_fields, type_name, schema));
}
} else {
out.push(sel);
}
}
out
}
#[cfg(test)]
#[path = "projection_tests.rs"]
mod tests;