blob: a24d2882a05e5ae8683fd3e815b8e47644896837 [file] [log] [blame]
// Copyright 2019 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.
use serde::{Deserialize, Serialize};
use serde_json::{from_reader, from_str, to_string, to_value};
use std::io::Read;
use thiserror::Error;
use valico::common::error::ValicoError;
use valico::json_schema;
/// The various types of errors raised by this module.
#[derive(Debug, Error)]
enum Error {
#[error("invalid JSON schema: {:?}", _0)]
SchemaInvalid(valico::json_schema::schema::SchemaError),
#[error("could not validate file: {}", errors)]
#[allow(dead_code)] // The compiler complains about the variant not being constructed.
JsonFileInvalid { errors: String },
}
type Result<T> = std::result::Result<T, anyhow::Error>;
impl From<valico::json_schema::schema::SchemaError> for Error {
fn from(err: valico::json_schema::schema::SchemaError) -> Self {
Error::SchemaInvalid(err)
}
}
/// Generates user-friendly messages for validation errors.
fn format_valico_error(error: &Box<dyn ValicoError>) -> String {
// $title[at $path][: $detail] ($code)
let mut result = String::new();
result.push_str(error.get_title());
let path = error.get_path();
if !path.is_empty() {
result.push_str(" at ");
result.push_str(path);
}
if let Some(detail) = error.get_detail() {
result.push_str(": ");
result.push_str(detail);
}
result.push_str(" (");
result.push_str(error.get_code());
result.push_str(")");
result
}
/// Augments metadata representations with utility methods to serialize/deserialize and validate
/// their contents.
pub trait JsonObject: for<'a> Deserialize<'a> + Serialize + Sized {
/// Creates a new instance from its raw data.
fn new<R: Read>(source: R) -> Result<Self> {
Ok(from_reader(source)?)
}
/// Returns the schema matching the object type.
fn get_schema() -> &'static str;
/// Checks whether the object satisfies its associated JSON schema.
fn validate(&self) -> Result<()> {
let schema = from_str(Self::get_schema())?;
let mut scope = json_schema::Scope::new();
// Add the schema including all the common definitions.
let common_schema = from_str(include_str!("../common.json"))?;
scope.compile(common_schema, true).map_err(Error::SchemaInvalid)?;
let validator = scope.compile_and_return(schema, true).map_err(Error::SchemaInvalid)?;
let value = to_value(self)?;
let result = validator.validate(&value);
if !result.is_valid() {
let mut error_messages: Vec<String> =
result.errors.iter().map(format_valico_error).collect();
error_messages.sort_unstable();
return Err(Error::JsonFileInvalid { errors: error_messages.join(", ") }.into());
}
Ok(())
}
/// Serializes the object into its string representation.
fn to_string(&self) -> Result<String> {
Ok(to_string(self)?)
}
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use super::*;
#[derive(Deserialize, Serialize)]
struct Metadata {
target: String,
}
impl JsonObject for Metadata {
fn get_schema() -> &'static str {
r#"{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://fuchsia.com/schemas/sdk/test_metadata.json",
"properties": {
"target": {
"$ref": "common.json#/definitions/target_arch"
}
}
}"#
}
}
#[test]
/// Checks that references to common.json are properly resolved.
fn test_common_reference() {
let metadata = Metadata {
target: "y128".to_string(), // Not a valid architecture.
};
let result = metadata.validate();
assert!(result.is_err(), "Validation did not respect common schema.");
}
}