use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt;
use primitives_rs::tsgen::{
self, FieldDef, FieldPresence, TsGen, TypeDef, TypePhase, TypeRef, TypeRegistry,
};
use serde::Serialize;
use serde::ser::{SerializeMap, Serializer};
use serde_json::{Map, Number, Value};
const ESCAPE_PREFIX: char = '~';
const KEY_SEPARATOR: char = '.';
const ESCAPED_TILDE: &str = "~0";
const ESCAPED_KEY_SEPARATOR: &str = "~1";
const EMPTY_KEY_SEGMENT: &str = "~2";
const NULL_MARKER: &str = "~n";
const EMPTY_ARRAY_MARKER: &str = "~a";
const EMPTY_OBJECT_MARKER: &str = "~o";
const ESCAPED_SCALAR_PREFIX: &str = "~s";
pub const VIEW_PAYLOAD_QUERY_KEY: &str = "vorma-json";
pub const DEPLOYMENT_QUERY_KEY: &str = "dpl";
#[derive(Clone, Debug, PartialEq)]
pub enum SearchShape {
Scalar(SearchScalar),
Object {
fields: Vec<SearchField>,
nullable: bool,
},
Array {
item: SearchScalar,
nullable: bool,
},
Record {
permitted_keys: PermittedStrings,
value: Box<SearchShape>,
nullable: bool,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PermittedStrings {
AnyString,
OneOf(Vec<String>),
}
impl PermittedStrings {
fn to_list(&self) -> Option<&[String]> {
match self {
Self::AnyString => None,
Self::OneOf(strings) => Some(strings),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SearchScalar {
pub kind: SearchScalarKind,
pub permitted_strings: PermittedStrings,
pub nullable: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SearchScalarKind {
String,
Number,
Boolean,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SearchField {
pub name: String,
pub shape: SearchShape,
pub optional: bool,
}
impl SearchShape {
pub(crate) fn has_top_level_field(&self, name: &str) -> bool {
matches!(self, Self::Object { fields, .. } if fields.iter().any(|field| field.name == name))
}
fn is_nullable(&self) -> bool {
match self {
Self::Scalar(scalar) => scalar.nullable,
Self::Object { nullable, .. }
| Self::Array { nullable, .. }
| Self::Record { nullable, .. } => *nullable,
}
}
fn into_nullable(mut self) -> Self {
match &mut self {
Self::Scalar(SearchScalar { nullable, .. })
| Self::Object { nullable, .. }
| Self::Array { nullable, .. }
| Self::Record { nullable, .. } => *nullable = true,
}
self
}
fn serialize_with_optional<S: Serializer>(
&self,
serializer: S,
optional: bool,
) -> Result<S::Ok, S::Error> {
let mut map = match self {
Self::Scalar(scalar) => {
return ScalarJson::of(scalar, optional).serialize(serializer);
}
Self::Object { fields, .. } => {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("kind", "object")?;
map.serialize_entry("fields", &FieldsJson(fields))?;
map
}
Self::Array { item, .. } => {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("kind", "array")?;
map.serialize_entry("collection", "array")?;
map.serialize_entry("item", &ScalarJson::of(item, false))?;
map
}
Self::Record {
permitted_keys,
value,
..
} => {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("kind", "record")?;
map.serialize_entry("collection", "record")?;
map.serialize_entry(
"key",
&ScalarJson {
kind: SearchScalarKind::String,
values: permitted_keys.to_list(),
nullable: false,
optional: false,
},
)?;
map.serialize_entry("value", value.as_ref())?;
map
}
};
if self.is_nullable() {
map.serialize_entry("nullable", &true)?;
}
if optional {
map.serialize_entry("optional", &true)?;
}
map.end()
}
}
impl Serialize for SearchShape {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.serialize_with_optional(serializer, false)
}
}
#[derive(Serialize)]
#[serde(tag = "kind", rename = "scalar")]
struct ScalarJson<'a> {
#[serde(rename = "type")]
kind: SearchScalarKind,
#[serde(skip_serializing_if = "Option::is_none")]
values: Option<&'a [String]>,
#[serde(skip_serializing_if = "is_false")]
nullable: bool,
#[serde(skip_serializing_if = "is_false")]
optional: bool,
}
impl<'a> ScalarJson<'a> {
fn of(scalar: &'a SearchScalar, optional: bool) -> Self {
Self {
kind: scalar.kind,
values: scalar.permitted_strings.to_list(),
nullable: scalar.nullable,
optional,
}
}
}
fn is_false(value: &bool) -> bool {
!value
}
struct FieldsJson<'a>(&'a [SearchField]);
impl Serialize for FieldsJson<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_map(
self.0
.iter()
.map(|field| (field.name.as_str(), FieldJson(field))),
)
}
}
struct FieldJson<'a>(&'a SearchField);
impl Serialize for FieldJson<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0
.shape
.serialize_with_optional(serializer, self.0.optional)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SearchShapeError {
Defs(tsgen::Error),
ReservedName {
name: String,
},
Unsupported {
path: String,
reason: String,
},
}
impl fmt::Display for SearchShapeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Defs(error) => write!(f, "{error}"),
Self::ReservedName { name } => write!(
f,
"field {name:?} uses a reserved name; Vorma's wire protocol reserves {VIEW_PAYLOAD_QUERY_KEY:?}"
),
Self::Unsupported { path, reason } if path.is_empty() => {
write!(f, "unsupported search input: {reason}")
}
Self::Unsupported { path, reason } => {
write!(f, "unsupported search input field {path:?}: {reason}")
}
}
}
}
impl Error for SearchShapeError {}
fn unsupported_error(path: &str, reason: impl Into<String>) -> SearchShapeError {
SearchShapeError::Unsupported {
path: path.to_owned(),
reason: reason.into(),
}
}
pub fn derive_search_shape<T: TsGen>() -> Result<SearchShape, SearchShapeError> {
let mut registry = TypeRegistry::default();
T::collect_type_defs_for(TypePhase::Deserialize, &mut registry)
.map_err(SearchShapeError::Defs)?;
let defs = registry.into_defs();
let root = T::type_ref_for(TypePhase::Deserialize);
let shape = ShapeDerivation {
defs: &defs,
visiting: Vec::new(),
}
.derive(&root, "")?;
if let SearchShape::Object {
fields,
nullable: false,
} = &shape
&& let Some(field) = fields
.iter()
.find(|field| field.name == VIEW_PAYLOAD_QUERY_KEY)
{
return Err(SearchShapeError::ReservedName {
name: field.name.clone(),
});
}
match shape {
SearchShape::Object {
nullable: false, ..
}
| SearchShape::Record {
nullable: false, ..
} => Ok(shape),
_ => Err(unsupported_error(
"",
"the search input must deserialize from a structural object",
)),
}
}
#[derive(Clone, Copy)]
struct NamedTypeRef<'a> {
key: &'a str,
name: &'a str,
}
struct ShapeDerivation<'a> {
defs: &'a [TypeDef],
visiting: Vec<&'a str>,
}
impl<'a> ShapeDerivation<'a> {
fn derive(
&mut self,
type_ref: &'a TypeRef,
path: &str,
) -> Result<SearchShape, SearchShapeError> {
let no_spelling =
|type_name: &str| unsupported_error(path, format!("{type_name} has no URL spelling"));
match type_ref {
TypeRef::Bool => Ok(scalar_shape(
SearchScalarKind::Boolean,
PermittedStrings::AnyString,
)),
TypeRef::String => Ok(scalar_shape(
SearchScalarKind::String,
PermittedStrings::AnyString,
)),
TypeRef::Number | TypeRef::Integer => Ok(scalar_shape(
SearchScalarKind::Number,
PermittedStrings::AnyString,
)),
TypeRef::StringLiteral(value) => Ok(scalar_shape(
SearchScalarKind::String,
PermittedStrings::OneOf(vec![value.clone()]),
)),
TypeRef::Nullable(inner) => create_nullable_shape(self.derive(inner, path)?, path),
TypeRef::Union(members) => self.derive_union(members, path),
TypeRef::Array(item) => match self.derive(item, path)? {
SearchShape::Scalar(item) => Ok(SearchShape::Array {
item,
nullable: false,
}),
_ => Err(unsupported_error(
path,
"array items must have one scalar URL spelling",
)),
},
TypeRef::Map(key, value) => {
let SearchShape::Scalar(SearchScalar {
kind: SearchScalarKind::String,
permitted_strings: permitted_keys,
nullable: false,
}) = self.derive(key, path)?
else {
return Err(unsupported_error(
path,
"map keys must be strings or string enums",
));
};
let value = self.derive(value, &format!("{path}{KEY_SEPARATOR}*"))?;
Ok(SearchShape::Record {
permitted_keys,
value: Box::new(value),
nullable: false,
})
}
TypeRef::Object(fields) => self.derive_object(fields, path),
TypeRef::Intersection(members) => self.derive_intersection(members, path),
TypeRef::Named { key, name } => self.derive_named(NamedTypeRef { key, name }, path),
TypeRef::Tuple(_) => Err(no_spelling("a tuple")),
TypeRef::Unit => Err(no_spelling("undefined")),
TypeRef::Null => Err(no_spelling("null")),
TypeRef::Unknown => Err(no_spelling("unknown")),
TypeRef::WideInteger { rust_type } => Err(unsupported_error(
path,
format!(
"the Rust integer type {rust_type} has no URL spelling that JavaScript reads exactly. Use IntegerString<{rust_type}>, which is decimal text, or a 32-bit integer type (i32 or u32) when the range allows"
),
)),
TypeRef::FormData => Err(no_spelling("FormData")),
TypeRef::Blob => Err(no_spelling("Blob")),
TypeRef::Raw(_) => Err(no_spelling("a raw TypeScript type")),
}
}
fn derive_union(
&mut self,
members: &'a [TypeRef],
path: &str,
) -> Result<SearchShape, SearchShapeError> {
let carried = members
.iter()
.filter(|member| !matches!(member, TypeRef::Null))
.collect::<Vec<_>>();
let shape = match carried.as_slice() {
[] => {
return Err(unsupported_error(
path,
"a union of only null carries nothing",
));
}
[member] => self.derive(member, path)?,
_ => {
let values = carried
.iter()
.map(|member| match member {
TypeRef::StringLiteral(value) => Ok(value.clone()),
_ => Err(unsupported_error(
path,
"unions other than string literal enums cannot be search parameters",
)),
})
.collect::<Result<_, _>>()?;
scalar_shape(SearchScalarKind::String, PermittedStrings::OneOf(values))
}
};
if carried.len() < members.len() {
create_nullable_shape(shape, path)
} else {
Ok(shape)
}
}
fn derive_named(
&mut self,
named: NamedTypeRef<'a>,
path: &str,
) -> Result<SearchShape, SearchShapeError> {
let NamedTypeRef { key, name } = named;
if self.visiting.contains(&key) {
return Err(unsupported_error(
path,
"recursive types cannot be search parameters",
));
}
let defs = self.defs;
let Some(def) = defs.iter().find(|def| def.key() == key) else {
return Err(unsupported_error(
path,
format!("the type {name} has no collected definition"),
));
};
self.visiting.push(key);
let shape = self.derive(def.type_ref(), path);
self.visiting.pop();
shape
}
fn derive_object(
&mut self,
fields: &'a [FieldDef],
path: &str,
) -> Result<SearchShape, SearchShapeError> {
let mut fields = fields
.iter()
.map(|field| {
let field_path = if path.is_empty() {
field.name().to_owned()
} else {
format!("{path}{KEY_SEPARATOR}{}", field.name())
};
Ok(SearchField {
name: field.name().to_owned(),
shape: self.derive(field.type_ref(), &field_path)?,
optional: field.presence() == FieldPresence::Optional,
})
})
.collect::<Result<Vec<_>, _>>()?;
order_like_javascript(&mut fields);
Ok(SearchShape::Object {
fields,
nullable: false,
})
}
fn derive_intersection(
&mut self,
members: &'a [TypeRef],
path: &str,
) -> Result<SearchShape, SearchShapeError> {
let mut fields: Vec<SearchField> = Vec::new();
for member in members {
let SearchShape::Object {
fields: member_fields,
nullable: false,
} = self.derive(member, path)?
else {
return Err(unsupported_error(
path,
"a flattened field must be a struct that is not optional",
));
};
for field in member_fields {
if fields.iter().any(|existing| existing.name == field.name) {
return Err(unsupported_error(
path,
format!("the field {} appears more than once", field.name),
));
}
fields.push(field);
}
}
order_like_javascript(&mut fields);
Ok(SearchShape::Object {
fields,
nullable: false,
})
}
}
fn order_like_javascript(fields: &mut [SearchField]) {
fields.sort_by_key(|field| parse_array_index(&field.name).map_or((1, 0), |index| (0, index)));
}
fn create_nullable_shape(shape: SearchShape, path: &str) -> Result<SearchShape, SearchShapeError> {
if shape.is_nullable() {
return Err(unsupported_error(
path,
"Option<Option<T>> cannot round-trip through a URL, because a query spells both \
None values as ~n. Use Option<T>",
));
}
if let SearchShape::Array { item, .. } = &shape
&& item.nullable
{
return Err(unsupported_error(
path,
"Option<Vec<Option<T>>> cannot round-trip through a URL, because a query spells \
a null array and a null item both as ~n. Use Vec<Option<T>> when items can be \
null, or Option<Vec<T>> when the array can be null",
));
}
Ok(shape.into_nullable())
}
fn scalar_shape(kind: SearchScalarKind, permitted_strings: PermittedStrings) -> SearchShape {
SearchShape::Scalar(SearchScalar {
kind,
permitted_strings,
nullable: false,
})
}
fn parse_array_index(name: &str) -> Option<u32> {
let index = name.parse::<u32>().ok()?;
(index != u32::MAX && index.to_string() == name).then_some(index)
}
#[derive(Clone, Copy)]
struct EnteredText<'a> {
raw: &'a str,
path: &'a str,
}
struct Expected(&'static str);
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SearchParamsError {
Missing {
path: String,
},
Invalid {
path: String,
expected: &'static str,
},
}
impl fmt::Display for SearchParamsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Missing { path } => write!(f, "search parameter {path:?} is missing"),
Self::Invalid { path, expected } => {
write!(
f,
"search parameter {path:?} is invalid; expected {expected}"
)
}
}
}
}
impl Error for SearchParamsError {}
pub fn parse_search_params(
shape: &SearchShape,
entries: &[(String, String)],
) -> Result<Value, SearchParamsError> {
QueryIndex::new(entries).parse(shape, "")
}
struct QueryIndex<'a> {
values: HashMap<&'a str, Vec<&'a str>>,
children: HashMap<&'a str, Vec<&'a str>>,
}
impl<'a> QueryIndex<'a> {
fn new(entries: &'a [(String, String)]) -> Self {
let mut values = HashMap::<&str, Vec<&str>>::new();
let mut children = HashMap::<&str, Vec<&str>>::new();
let mut indexed = HashSet::new();
for (key, value) in entries {
if let Some(existing) = values.get_mut(key.as_str()) {
existing.push(value);
continue;
}
values.insert(key, vec![value]);
if key.is_empty() {
continue;
}
let mut parent = "";
let mut start = 0;
loop {
let separator = key[start..]
.find(KEY_SEPARATOR)
.map(|offset| start + offset);
let child = &key[start..separator.unwrap_or(key.len())];
if (!parent.is_empty() || start == 0) && indexed.insert((parent, child)) {
children.entry(parent).or_default().push(child);
}
let Some(separator) = separator else {
break;
};
parent = &key[..separator];
start = separator + KEY_SEPARATOR.len_utf8();
}
}
Self { values, children }
}
fn parse(&self, shape: &SearchShape, path: &str) -> Result<Value, SearchParamsError> {
if shape.is_nullable() {
if !self.is_present(shape, path) {
return Ok(Value::Null);
}
let exact = self.values_at(path);
if let SearchShape::Scalar(_) = shape {
if exact.first() == Some(&NULL_MARKER) {
return Ok(Value::Null);
}
} else if exact.contains(&NULL_MARKER) {
return if exact.len() == 1 && !self.has_children(path) {
Ok(Value::Null)
} else {
Err(invalid_error(path, Expected("an unambiguous null marker")))
};
}
}
match shape {
SearchShape::Scalar(scalar) => {
let Some(&raw) = self.values_at(path).first() else {
return Err(missing_error(path));
};
if raw.is_empty() && scalar.kind != SearchScalarKind::String {
return Err(missing_error(path));
}
parse_scalar(scalar, EnteredText { raw, path })
}
SearchShape::Object { fields, .. } => self.parse_object(fields, path),
SearchShape::Array { item, .. } => self.parse_array(item, path),
SearchShape::Record {
permitted_keys,
value,
..
} => self.parse_record(permitted_keys, value, path),
}
}
fn parse_object(&self, fields: &[SearchField], path: &str) -> Result<Value, SearchParamsError> {
let entered = self.entered_values(path);
if !entered.is_empty() && (entered != [EMPTY_OBJECT_MARKER] || self.has_children(path)) {
return Err(invalid_error(
path,
Expected("an unambiguous empty-object marker or child fields"),
));
}
let mut object = Map::new();
for field in fields {
let field_path = append_key(path, FieldName(&field.name));
if field.optional && !self.is_present(&field.shape, &field_path) {
continue;
}
object.insert(field.name.clone(), self.parse(&field.shape, &field_path)?);
}
Ok(Value::Object(object))
}
fn parse_array(&self, item: &SearchScalar, path: &str) -> Result<Value, SearchParamsError> {
let entered = self.entered_values(path);
if entered.is_empty() || entered == [EMPTY_ARRAY_MARKER] {
return Ok(Value::Array(Vec::new()));
}
if entered.contains(&EMPTY_ARRAY_MARKER) {
return Err(invalid_error(
path,
Expected("an array without a mixed empty-array marker"),
));
}
entered
.into_iter()
.map(|raw| parse_scalar(item, EnteredText { raw, path }))
.collect::<Result<_, _>>()
.map(Value::Array)
}
fn parse_record(
&self,
permitted_keys: &PermittedStrings,
value: &SearchShape,
path: &str,
) -> Result<Value, SearchParamsError> {
let entered = self.entered_values(path);
if entered == [EMPTY_OBJECT_MARKER] && !self.has_children(path) {
return Ok(Value::Object(Map::new()));
}
if !entered.is_empty() {
return Err(invalid_error(
path,
Expected("an empty-object marker or dotted map entries"),
));
}
let mut record = Map::new();
for &encoded_key in self.children.get(path).into_iter().flatten() {
if encoded_key.is_empty() {
continue;
}
let key = decode_key_segment(EnteredText {
raw: encoded_key,
path,
})?;
if matches!(permitted_keys, PermittedStrings::OneOf(keys) if !keys.contains(&key)) {
return Err(invalid_error(path, Expected("a declared enum variant")));
}
let entry_path = if path.is_empty() {
encoded_key.to_owned()
} else {
format!("{path}{KEY_SEPARATOR}{encoded_key}")
};
if self.is_entry_empty(value, &entry_path) {
continue;
}
record.insert(key, self.parse(value, &entry_path)?);
}
Ok(Value::Object(record))
}
fn values_at(&self, path: &str) -> &[&'a str] {
self.values.get(path).map_or(&[], Vec::as_slice)
}
fn entered_values(&self, path: &str) -> Vec<&'a str> {
self.values_at(path)
.iter()
.copied()
.filter(|value| !value.is_empty())
.collect()
}
fn has_children(&self, path: &str) -> bool {
self.children.contains_key(path)
}
fn has_child_keys(&self, shape: &SearchShape, path: &str) -> bool {
!matches!(shape, SearchShape::Scalar(_)) && self.has_children(path)
}
fn is_present(&self, shape: &SearchShape, path: &str) -> bool {
(self.values.contains_key(path) || self.has_child_keys(shape, path))
&& !self.is_entry_empty(shape, path)
}
fn is_entry_empty(&self, shape: &SearchShape, path: &str) -> bool {
if self.has_child_keys(shape, path) {
return false;
}
let values = self.values_at(path);
match shape {
SearchShape::Scalar(_) => values.first() == Some(&""),
_ => !values.is_empty() && values.iter().all(|value| value.is_empty()),
}
}
}
fn parse_scalar(
scalar: &SearchScalar,
entered: EnteredText<'_>,
) -> Result<Value, SearchParamsError> {
let EnteredText { raw, path } = entered;
if raw == NULL_MARKER && scalar.nullable {
return Ok(Value::Null);
}
let text = decode_scalar(EnteredText { raw, path })?;
match scalar.kind {
SearchScalarKind::String => match &scalar.permitted_strings {
PermittedStrings::OneOf(strings) if !strings.iter().any(|string| string == text) => {
Err(invalid_error(path, Expected("a declared enum variant")))
}
_ => Ok(Value::String(text.to_owned())),
},
SearchScalarKind::Boolean => match text.to_lowercase().as_str() {
"true" | "1" | "on" => Ok(Value::Bool(true)),
"false" | "0" => Ok(Value::Bool(false)),
_ => Err(invalid_error(path, Expected("true, false, 1, 0, or on"))),
},
SearchScalarKind::Number => parse_number(EnteredText { raw: text, path }),
}
}
fn parse_number(entered: EnteredText<'_>) -> Result<Value, SearchParamsError> {
let EnteredText { raw: text, path } = entered;
let number = is_decimal_number(text)
.then(|| text.parse::<f64>().ok())
.flatten()
.filter(|number| number.is_finite())
.ok_or_else(|| invalid_error(path, Expected("a finite decimal number")))?;
let integer_range = i64::MIN as f64..-(i64::MIN as f64);
Ok(
if number.fract() == 0.0 && integer_range.contains(&number) {
Value::from(number as i64)
} else {
Value::from(number)
},
)
}
fn is_decimal_number(text: &str) -> bool {
let digits = |text: &str| !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit());
let unsigned = text.strip_prefix(['+', '-']).unwrap_or(text);
let (mantissa, exponent) = match unsigned.split_once(['e', 'E']) {
Some((mantissa, exponent)) => (mantissa, Some(exponent)),
None => (unsigned, None),
};
let mantissa_is_valid = match mantissa.split_once('.') {
Some((whole, fraction)) => {
(digits(whole) && (fraction.is_empty() || digits(fraction)))
|| (whole.is_empty() && digits(fraction))
}
None => digits(mantissa),
};
mantissa_is_valid
&& exponent
.is_none_or(|exponent| digits(exponent.strip_prefix(['+', '-']).unwrap_or(exponent)))
}
fn decode_key_segment(entered: EnteredText<'_>) -> Result<String, SearchParamsError> {
let EnteredText { raw: encoded, path } = entered;
if encoded == EMPTY_KEY_SEGMENT {
return Ok(String::new());
}
let mut decoded = String::with_capacity(encoded.len());
let mut characters = encoded.chars();
while let Some(character) = characters.next() {
if character != ESCAPE_PREFIX {
decoded.push(character);
continue;
}
match characters.next() {
Some('0') => decoded.push(ESCAPE_PREFIX),
Some('1') => decoded.push(KEY_SEPARATOR),
_ => {
return Err(invalid_error(path, Expected("a well-formed key escape")));
}
}
}
Ok(decoded)
}
fn decode_scalar(entered: EnteredText<'_>) -> Result<&str, SearchParamsError> {
let EnteredText { raw, path } = entered;
if let Some(escaped) = raw.strip_prefix(ESCAPED_SCALAR_PREFIX) {
return if escaped.is_empty() || escaped.starts_with(ESCAPE_PREFIX) {
Ok(escaped)
} else {
Err(invalid_error(
path,
Expected("a well-formed escaped scalar"),
))
};
}
if raw.starts_with(ESCAPE_PREFIX) {
Err(invalid_error(
path,
Expected("a scalar, not a reserved marker"),
))
} else {
Ok(raw)
}
}
fn missing_error(path: &str) -> SearchParamsError {
SearchParamsError::Missing {
path: path.to_owned(),
}
}
fn invalid_error(path: &str, expected: Expected) -> SearchParamsError {
SearchParamsError::Invalid {
path: path.to_owned(),
expected: expected.0,
}
}
pub fn serialize_search_params(value: &Value) -> Vec<(String, String)> {
let mut entries = Vec::new();
append_value(&mut entries, "", value);
entries
}
fn append_value(entries: &mut Vec<(String, String)>, key: &str, value: &Value) {
let text = match value {
Value::Null => NULL_MARKER.to_owned(),
Value::Array(items) if items.is_empty() => EMPTY_ARRAY_MARKER.to_owned(),
Value::Object(fields) if fields.is_empty() => EMPTY_OBJECT_MARKER.to_owned(),
Value::Array(items) => {
for item in items {
append_value(entries, key, item);
}
return;
}
Value::Object(fields) => {
let mut fields = fields.iter().collect::<Vec<_>>();
fields.sort_by(|(left, _), (right, _)| left.encode_utf16().cmp(right.encode_utf16()));
for (name, child) in fields {
append_value(entries, &append_key(key, FieldName(name)), child);
}
return;
}
Value::Bool(boolean) => boolean.to_string(),
Value::Number(number) => format_number(number),
Value::String(text) if text.is_empty() => ESCAPED_SCALAR_PREFIX.to_owned(),
Value::String(text) if text.starts_with(ESCAPE_PREFIX) => {
format!("{ESCAPED_SCALAR_PREFIX}{text}")
}
Value::String(text) => text.clone(),
};
entries.push((key.to_owned(), text));
}
struct FieldName<'a>(&'a str);
fn append_key(parent: &str, field_name: FieldName<'_>) -> String {
let child = field_name.0;
let encoded = if child.is_empty() {
EMPTY_KEY_SEGMENT.to_owned()
} else {
child
.replace(ESCAPE_PREFIX, ESCAPED_TILDE)
.replace(KEY_SEPARATOR, ESCAPED_KEY_SEPARATOR)
};
if parent.is_empty() {
encoded
} else {
format!("{parent}{KEY_SEPARATOR}{encoded}")
}
}
fn format_number(number: &Number) -> String {
let Some(float) = number.as_f64().filter(|_| number.is_f64()) else {
return number.to_string();
};
if float == 0.0 {
return "0".to_owned();
}
let magnitude = float.abs();
let shortest = format!("{magnitude:e}");
let (shortest_digits, _) = split_scientific(&shortest);
let nearest = format!("{magnitude:.*e}", shortest_digits.len() - 1);
let (digits, exponent) = if nearest.parse() == Ok(magnitude) {
split_scientific(&nearest)
} else {
split_scientific(&shortest)
};
let sign = if float < 0.0 { "-" } else { "" };
let digit_count = digits.len() as i32;
let point = exponent + 1;
if (digit_count..=21).contains(&point) {
let zeros = "0".repeat((point - digit_count) as usize);
format!("{sign}{digits}{zeros}")
} else if (1..=21).contains(&point) {
let (whole, fraction) = digits.split_at(point as usize);
format!("{sign}{whole}.{fraction}")
} else if (-5..=0).contains(&point) {
let zeros = "0".repeat(-point as usize);
format!("{sign}0.{zeros}{digits}")
} else {
let (first, rest) = digits.split_at(1);
let separator = if rest.is_empty() { "" } else { "." };
format!("{sign}{first}{separator}{rest}e{exponent:+}")
}
}
fn split_scientific(text: &str) -> (String, i32) {
let (mantissa, exponent) = text
.split_once('e')
.expect("scientific notation has an exponent");
let exponent = exponent
.parse()
.expect("scientific notation has an integer exponent");
(mantissa.replace('.', ""), exponent)
}