blob: 6ba922046e41f588c784b62dd1e0012ad975a9b9 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Validating and encoding individual configuration fields.
use cm_rust::{ConfigNestedValueType, ConfigValueType, SingleValue, Value, VectorValue};
use serde_json::Value as JsonValue;
use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult},
num::TryFromIntError,
};
/// Returns a FIDL value to encode in the file for the provided JSON value, if and only if the value
/// matches the type from the declaration.
pub fn config_value_from_json_value(
val: &JsonValue,
value_type: &ConfigValueType,
) -> Result<Value, FieldError> {
Ok(match value_type {
ConfigValueType::Bool => Value::Single(SingleValue::Bool(val.parse_bool()?)),
ConfigValueType::Uint8 => Value::Single(SingleValue::Uint8(val.parse_u8()?)),
ConfigValueType::Uint16 => Value::Single(SingleValue::Uint16(val.parse_u16()?)),
ConfigValueType::Uint32 => Value::Single(SingleValue::Uint32(val.parse_u32()?)),
ConfigValueType::Uint64 => Value::Single(SingleValue::Uint64(val.parse_u64()?)),
ConfigValueType::Int8 => Value::Single(SingleValue::Int8(val.parse_i8()?)),
ConfigValueType::Int16 => Value::Single(SingleValue::Int16(val.parse_i16()?)),
ConfigValueType::Int32 => Value::Single(SingleValue::Int32(val.parse_i32()?)),
ConfigValueType::Int64 => Value::Single(SingleValue::Int64(val.parse_i64()?)),
ConfigValueType::String { max_size } => {
Value::Single(SingleValue::String(val.parse_string(*max_size)?))
}
ConfigValueType::Vector { max_count, nested_type } => {
Value::Vector(vector_value_from_json(val, max_count, nested_type)?)
}
})
}
/// Parse `val` as a vector of configuration values.
fn vector_value_from_json(
val: &JsonValue,
max_count: &u32,
nested_type: &ConfigNestedValueType,
) -> Result<VectorValue, FieldError> {
// define our array up here so its identifier is available for our helper macro
let array = val.as_array().ok_or_else(|| FieldError::JsonTypeMismatch {
expected: JsonTy::Array,
received: val.ty(),
})?;
let max = *max_count as usize;
if array.len() > max {
return Err(FieldError::VectorTooLong { max, actual: array.len() });
}
/// Build a VectorValue out of all the array elements.
///
/// A macro because enum variants don't exist at the type level in Rust at time of writing.
macro_rules! vector_from_array {
($list_variant:ident, $val:ident => $convert:expr) => {{
let mut list = vec![];
for $val in array {
list.push($convert);
}
VectorValue::$list_variant(list)
}};
}
Ok(match nested_type {
ConfigNestedValueType::Bool => {
vector_from_array!(BoolVector, v => v.parse_bool()?)
}
ConfigNestedValueType::Uint8 => vector_from_array!(Uint8Vector, v => v.parse_u8()?),
ConfigNestedValueType::Uint16 => {
vector_from_array!(Uint16Vector, v => v.parse_u16()?)
}
ConfigNestedValueType::Uint32 => {
vector_from_array!(Uint32Vector, v => v.parse_u32()?)
}
ConfigNestedValueType::Uint64 => {
vector_from_array!(Uint64Vector, v => v.parse_u64()?)
}
ConfigNestedValueType::Int8 => vector_from_array!(Int8Vector, v => v.parse_i8()?),
ConfigNestedValueType::Int16 => vector_from_array!(Int16Vector, v => v.parse_i16()?),
ConfigNestedValueType::Int32 => vector_from_array!(Int32Vector, v => v.parse_i32()?),
ConfigNestedValueType::Int64 => vector_from_array!(Int64Vector, v => v.parse_i64()?),
ConfigNestedValueType::String { max_size } => {
vector_from_array!(StringVector, v => v.parse_string(*max_size)?)
}
})
}
trait JsonValueExt {
fn parse_bool(&self) -> Result<bool, FieldError>;
fn parse_u8(&self) -> Result<u8, FieldError>;
fn parse_u16(&self) -> Result<u16, FieldError>;
fn parse_u32(&self) -> Result<u32, FieldError>;
fn parse_u64(&self) -> Result<u64, FieldError>;
fn parse_i8(&self) -> Result<i8, FieldError>;
fn parse_i16(&self) -> Result<i16, FieldError>;
fn parse_i32(&self) -> Result<i32, FieldError>;
fn parse_i64(&self) -> Result<i64, FieldError>;
fn parse_string(&self, max: u32) -> Result<String, FieldError>;
fn ty(&self) -> JsonTy;
fn expected(&self, ty: JsonTy) -> FieldError;
}
fn check_integer(v: &JsonValue) -> Result<(), FieldError> {
if !v.is_number() {
Err(FieldError::JsonTypeMismatch { expected: JsonTy::Number, received: v.ty() })
} else if !(v.is_i64()) {
Err(FieldError::NumberNotInteger)
} else {
Ok(())
}
}
fn check_unsigned(v: &JsonValue) -> Result<(), FieldError> {
if v.is_u64() {
Ok(())
} else {
Err(FieldError::NumberNotUnsigned)
}
}
impl JsonValueExt for JsonValue {
fn parse_bool(&self) -> Result<bool, FieldError> {
self.as_bool().ok_or_else(|| self.expected(JsonTy::Bool))
}
fn parse_u8(&self) -> Result<u8, FieldError> {
check_integer(self)?;
check_unsigned(self)?;
Ok(<u8>::try_from(self.as_u64().ok_or_else(|| self.expected(JsonTy::Number))?)?)
}
fn parse_u16(&self) -> Result<u16, FieldError> {
check_integer(self)?;
check_unsigned(self)?;
Ok(<u16>::try_from(self.as_u64().ok_or_else(|| self.expected(JsonTy::Number))?)?)
}
fn parse_u32(&self) -> Result<u32, FieldError> {
check_integer(self)?;
check_unsigned(self)?;
Ok(<u32>::try_from(self.as_u64().ok_or_else(|| self.expected(JsonTy::Number))?)?)
}
fn parse_u64(&self) -> Result<u64, FieldError> {
check_integer(self)?;
check_unsigned(self)?;
self.as_u64().ok_or_else(|| self.expected(JsonTy::Number))
}
fn parse_i8(&self) -> Result<i8, FieldError> {
check_integer(self)?;
Ok(<i8>::try_from(self.as_i64().ok_or_else(|| self.expected(JsonTy::Number))?)?)
}
fn parse_i16(&self) -> Result<i16, FieldError> {
check_integer(self)?;
Ok(<i16>::try_from(self.as_i64().ok_or_else(|| self.expected(JsonTy::Number))?)?)
}
fn parse_i32(&self) -> Result<i32, FieldError> {
check_integer(self)?;
Ok(<i32>::try_from(self.as_i64().ok_or_else(|| self.expected(JsonTy::Number))?)?)
}
fn parse_i64(&self) -> Result<i64, FieldError> {
check_integer(self)?;
self.as_i64().ok_or_else(|| self.expected(JsonTy::Number))
}
fn parse_string(&self, max: u32) -> Result<String, FieldError> {
let max = max as usize;
let s = self.as_str().ok_or_else(|| FieldError::JsonTypeMismatch {
expected: JsonTy::String,
received: self.ty(),
})?;
if s.len() > max {
Err(FieldError::StringTooLong { max, actual: s.len() })
} else {
Ok(s.to_owned())
}
}
fn ty(&self) -> JsonTy {
match self {
JsonValue::Null => JsonTy::Null,
JsonValue::Bool(_) => JsonTy::Bool,
JsonValue::Number(_) => JsonTy::Number,
JsonValue::String(_) => JsonTy::String,
JsonValue::Array(_) => JsonTy::Array,
JsonValue::Object(_) => JsonTy::Object,
}
}
fn expected(&self, expected: JsonTy) -> FieldError {
FieldError::JsonTypeMismatch { expected, received: self.ty() }
}
}
/// Error from working with a field from a configuration value file.
#[derive(Debug, thiserror::Error, PartialEq)]
#[allow(missing_docs)]
pub enum FieldError {
#[error("Expected value of type {expected}, received {received}.")]
JsonTypeMismatch { expected: JsonTy, received: JsonTy },
#[error("Expected number to be unsigned.")]
NumberNotUnsigned,
#[error("Expected number to be an integer.")]
NumberNotInteger,
#[error("String of size {actual} provided for a field with maximum of {max}.")]
StringTooLong { max: usize, actual: usize },
#[error("Vector of count {actual} provided for a field with maximum of {max}.")]
VectorTooLong { max: usize, actual: usize },
#[error("Couldn't parse provided integer as expected type.")]
InvalidNumber(
#[from]
#[source]
TryFromIntError,
),
}
/// The types a [`serde_json::Value`] can have. Used for error reporting.
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum JsonTy {
Null,
Bool,
Number,
String,
Array,
Object,
}
impl Display for JsonTy {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
JsonTy::Null => write!(f, "null"),
JsonTy::Bool => write!(f, "bool"),
JsonTy::Number => write!(f, "number"),
JsonTy::String => write!(f, "string"),
JsonTy::Array => write!(f, "array"),
JsonTy::Object => write!(f, "object"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fidl_fuchsia_component_config_ext::config_ty;
use serde_json::json;
use FieldError::*;
fn try_from_int_error() -> TryFromIntError {
u64::try_from(-1i32).unwrap_err()
}
macro_rules! field_parse_tests {
(
mod: $mod_name:ident,
type: { $($type_toks:tt)+ },
tests: [$(
$eq_test_name:ident: $eq_input:expr => $eq_output:expr,
)+]
) => {
mod $mod_name {
use super::*;
// macro repetitions need to have the same nesting in expansion as they do in
// parsing, so we need a top-level function here rather than inside the tests repeat
fn __config_value_type() -> ConfigValueType {
config_ty!( $($type_toks)+ )
}
$(
#[test]
fn $eq_test_name() {
let value = $eq_input;
let config_ty = __config_value_type();
assert_eq!(
config_value_from_json_value(&value, &config_ty),
$eq_output,
);
}
)+
}
};
}
field_parse_tests! {
mod: parse_bool,
type: { bool },
tests: [
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Bool, received: JsonTy::Null }),
cant_be_number: json!(1) =>
Err(JsonTypeMismatch { expected: JsonTy::Bool, received: JsonTy::Number }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Bool, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Bool, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Bool, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_uint8,
type: { uint8 },
tests: [
cant_overflow: json!(256) => Err(InvalidNumber(try_from_int_error())),
cant_be_negative: json!(-1) =>
Err(NumberNotUnsigned),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_uint16,
type: { uint16 },
tests: [
cant_overflow: json!(65_536) => Err(InvalidNumber(try_from_int_error())),
cant_be_negative: json!(-1) =>
Err(NumberNotUnsigned),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_uint32,
type: { uint32 },
tests: [
cant_overflow: json!(4_294_967_296u64) => Err(InvalidNumber(try_from_int_error())),
cant_be_negative: json!(-1) =>
Err(NumberNotUnsigned),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_uint64,
type: { uint64 },
tests: [
cant_be_negative: json!(-1) =>
Err(NumberNotUnsigned),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_int8,
type: { int8 },
tests: [
cant_underflow: json!(-129) => Err(InvalidNumber(try_from_int_error())),
cant_overflow: json!(128) => Err(InvalidNumber(try_from_int_error())),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_int16,
type: { int16 },
tests: [
cant_underflow: json!(-32_769i32) => Err(InvalidNumber(try_from_int_error())),
cant_overflow: json!(32_768) => Err(InvalidNumber(try_from_int_error())),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_int32,
type: { int32 },
tests: [
cant_underflow: json!(-2_147_483_649i64) => Err(InvalidNumber(try_from_int_error())),
cant_overflow: json!(2_147_483_648i64) => Err(InvalidNumber(try_from_int_error())),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_int64,
type: { int64 },
tests: [
cant_overflow: json!(9_223_372_036_854_775_808u64) =>
Err(NumberNotInteger),
cant_be_float: json!(1.0) =>
Err(NumberNotInteger),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Bool }),
cant_be_string: json!("hello, world!") =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_string,
type: { string, max_size: 13 },
tests: [
can_be_empty: json!("") => Ok(Value::Single(SingleValue::String("".into()))),
max_length_fits: json!("hello, world!") =>
Ok(Value::Single(SingleValue::String("hello, world!".into()))),
cant_be_too_long: json!("1234567890 uhoh") =>
Err(StringTooLong { max: 13, actual: 15 }),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::String, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::String, received: JsonTy::Bool }),
cant_be_number: json!(1) =>
Err(JsonTypeMismatch { expected: JsonTy::String, received: JsonTy::Number }),
cant_be_array: json!([1, 2, 3]) =>
Err(JsonTypeMismatch { expected: JsonTy::String, received: JsonTy::Array }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::String, received: JsonTy::Object }),
]
}
field_parse_tests! {
mod: parse_vector,
type: { vector, element: int32, max_count: 5 },
tests: [
max_length_fits: json!([1, 2, 3, 4, 5]) =>
Ok(Value::Vector(VectorValue::Int32Vector(vec![1, 2, 3, 4, 5]))),
can_be_empty: json!([]) => Ok(Value::Vector(VectorValue::Int32Vector(vec![]))),
cant_be_too_long: json!([1, 2, 3, 4, 5, 6]) => Err(VectorTooLong { max: 5, actual: 6}),
element_type_must_match: json!(["foo"]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
needs_uniform_elements: json!([1, 2, "hello, world!"]) =>
Err(JsonTypeMismatch { expected: JsonTy::Number, received: JsonTy::String }),
cant_be_null: json!(null) =>
Err(JsonTypeMismatch { expected: JsonTy::Array, received: JsonTy::Null }),
cant_be_bool: json!(true) =>
Err(JsonTypeMismatch { expected: JsonTy::Array, received: JsonTy::Bool }),
cant_be_number: json!(1) =>
Err(JsonTypeMismatch { expected: JsonTy::Array, received: JsonTy::Number }),
cant_be_object: json!({"foo": 1, "bar": 2}) =>
Err(JsonTypeMismatch { expected: JsonTy::Array, received: JsonTy::Object }),
]
}
}