| // 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."); |
| } |
| } |