| // 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 anyhow::Result; |
| use serde::{Deserialize, Serialize}; |
| use serde_json::{from_reader, from_str, to_string, to_value}; |
| use std::io::Read; |
| use thiserror::Error; |
| use url::Url; |
| use valico::common::error::ValicoError; |
| use valico::json_schema; |
| |
| pub mod schema { |
| pub const COMMON: &str = include_str!("../common.json"); |
| pub const EMU_MANIFEST: &str = include_str!("../emu_manifest.json"); |
| pub const FLASH_MANIFEST_V1: &str = include_str!("../flash_manifest-835e8f26.json"); |
| pub const HARDWARE_V1: &str = include_str!("../hardware-f6f47515.json"); |
| pub const PRODUCT_BUNDLE_V1: &str = include_str!("../product_bundle-6320eef1.json"); |
| pub const PHYSICAL_DEVICE_V1: &str = include_str!("../physical_device-0bd5d21f.json"); |
| pub const VIRTUAL_DEVICE_V1: &str = include_str!("../virtual_device-93A41932.json"); |
| } |
| |
| /// 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 }, |
| } |
| |
| 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 |
| } |
| |
| /// Generates the missing schema reference message for the given URL. |
| fn format_missing_refs(url: &Url) -> String { |
| let mut result = String::new(); |
| result.push_str("Missing schema reference: "); |
| result.push_str(&url.to_string()); |
| 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; |
| |
| /// Returns a list of schemas referenced by this schema. |
| fn get_referenced_schemata() -> &'static [&'static str] { |
| &[schema::COMMON] |
| } |
| |
| // Returns schema ID. |
| fn get_schema_id() -> Result<String> { |
| let schema = from_str(Self::get_schema())?; |
| let mut scope = json_schema::Scope::new(); |
| let url = scope.compile(schema, /*ban_unknown=*/ true).map_err(Error::SchemaInvalid)?; |
| Ok(url.to_string()) |
| } |
| |
| /// 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. |
| for schema in Self::get_referenced_schemata() { |
| scope |
| .compile(from_str(schema)?, /*ban_unknown=*/ true) |
| .map_err(Error::SchemaInvalid)?; |
| } |
| |
| let validator = scope |
| .compile_and_return(schema, /*ban_unknown=*/ true) |
| .map_err(Error::SchemaInvalid)?; |
| let value = to_value(self)?; |
| let result = validator.validate(&value); |
| if !result.is_strictly_valid() { |
| let mut error_messages: Vec<String> = |
| result.errors.iter().map(format_valico_error).collect(); |
| error_messages.sort_unstable(); |
| |
| let mut missing_refs: Vec<String> = |
| result.missing.iter().map(format_missing_refs).collect(); |
| missing_refs.sort_unstable(); |
| error_messages.append(&mut missing_refs); |
| |
| 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] |
| fn test_id() { |
| assert_eq!( |
| String::from("http://fuchsia.com/schemas/sdk/test_metadata.json"), |
| Metadata::get_schema_id().expect("schema id") |
| ); |
| } |
| |
| #[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."); |
| } |
| } |