diff --git a/README.md b/README.md index d5b2cd8..604503a 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,16 @@ just service-doc {project-id} # Alternatively, you can use 'cd output/{project-i Remember to replace `{project-id}` with the name of your generated microservice (`title` field from the provided spec). +## Types +Rust types will be generated in the models folder according to the given payload json schema definitions. Names will be generated according to channels etc, if you want to share a payload type between two messages, make sure to use the same "name" property in the payload. Warning: This will not check if the types of those payloads are actually the same, so make sure to use the same schema or better even, simply a ref to the schema with the name. By default, all defined properties are required and no additional properties are allowed, if you want to use optional types, please modify the generated types after generation or use oneOf/anyOf/allOf to represent optional types. + ## Limitations - Only json payloads are currently supported for automatic deserialization - Only one server is currently supported and only nats protocol is supported - Only one message is currently supported per channel, payloads can be choosen freely including anyOf/oneOf/allOf +- The generated rust types are required by default, if you want to use optional types, please modify the generated types after generation or use oneOf/anyOf/allOf to represent optional types +- references in the specification are only suppported inside the same file, external references are not supported ## Contribute diff --git a/example/specs/basic.yaml b/example/specs/basic.yaml index 14f817f..1685424 100644 --- a/example/specs/basic.yaml +++ b/example/specs/basic.yaml @@ -14,23 +14,35 @@ channels: summary: User signup notification message: payload: + name: userSignUpPayload type: object properties: - userSingnedUp: + userName: type: string + minLength: 3 + password: + type: string + minLength: 8 + age: + type: number + minimum: 18 publish: operationId: userSignedUp summary: send welcome email to user message: - payload: + payload: + name: userSignUpPayload type: object properties: - username: + userName: type: string + minLength: 3 password: type: string + minLength: 8 age: type: number + minimum: 18 user/buy: subscribe: summary: User bought something diff --git a/src/asyncapi_model/message.rs b/src/asyncapi_model/message.rs index e728030..5074cc5 100644 --- a/src/asyncapi_model/message.rs +++ b/src/asyncapi_model/message.rs @@ -169,6 +169,9 @@ use super::{ #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Message { + #[serde(skip_serializing_if = "Option::is_none", rename = "schema")] + pub payload_schema: Option, + /// Schema definition of the application headers. /// Schema MUST be of type "object". It **MUST NOT** define the protocol headers. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/generator/common.rs b/src/generator/common.rs index 6371818..c884c42 100644 --- a/src/generator/common.rs +++ b/src/generator/common.rs @@ -8,7 +8,7 @@ use std::io::{self, Error}; use std::path::Path; use walkdir::WalkDir; -use super::generate_models_folder; +use super::{generate_models_folder, generate_schemas_folder}; /// runs cargo command with options /// Example: ` cargo_command!("init","--bin","path"); ` @@ -166,6 +166,10 @@ fn separate_files( generate_models_folder(template_str, context, output_dir); return Ok(true); } + if template_path.contains("$$schemas$$") { + generate_schemas_folder(template_str, context, output_dir); + return Ok(true); + } Ok(false) } diff --git a/src/generator/mod.rs b/src/generator/mod.rs index 813751a..add8528 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -6,4 +6,7 @@ pub use common::{ mod model; pub use model::generate_models_folder; +mod schemas; +pub use schemas::generate_schemas_folder; + mod template_functions; diff --git a/src/generator/schemas.rs b/src/generator/schemas.rs new file mode 100644 index 0000000..e080c94 --- /dev/null +++ b/src/generator/schemas.rs @@ -0,0 +1,33 @@ +use super::common::render_write_template; +use crate::parser::common::validate_identifier_string; +use crate::template_context::TemplateContext; +use std::path::Path; + +pub fn generate_schemas_folder( + template: impl Into + Clone, + async_config: &TemplateContext, + output_dir: &Path, +) { + async_config + .publish_channels + .iter() + .chain(async_config.subscribe_channels.iter()) + .for_each(|(_key, operation)| { + let message = operation.messages.first(); + if message.is_none() { + return; + } + let message = message.unwrap(); + if message.payload_schema.is_none() { + return; + } + render_write_template( + template.clone(), + &message.clone(), + &output_dir.join(format!( + "{}_payload_schema.json", + validate_identifier_string(&message.unique_id, false) + )), + ); + }); +} diff --git a/src/main.rs b/src/main.rs index cea249a..793e5dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,7 +51,6 @@ fn main() { } }; check_for_overwrite(output_path, title); - // make output a compilable project in output_path cargo_command!("init", "--bin", output_path); diff --git a/src/parser/asyncapi_model_parser/preprocessor.rs b/src/parser/asyncapi_model_parser/preprocessor.rs index 97c5a1f..e353b9e 100644 --- a/src/parser/asyncapi_model_parser/preprocessor.rs +++ b/src/parser/asyncapi_model_parser/preprocessor.rs @@ -6,8 +6,13 @@ use crate::parser::common::{self, validate_identifier_string}; pub fn preprocess_schema(spec: serde_json::Value) -> serde_json::Value { let with_message_names = fill_message_and_payload_names(spec.clone(), spec, false, false, None); let resolved_refs = resolve_refs(with_message_names.clone(), with_message_names); + let with_payload_schemas = duplicate_payload_schemas(resolved_refs.clone(), resolved_refs); let mut seen = HashSet::new(); - sanitize_operation_ids_and_check_duplicate(resolved_refs.clone(), resolved_refs, &mut seen) + sanitize_operation_ids_and_check_duplicate( + with_payload_schemas.clone(), + with_payload_schemas, + &mut seen, + ) } pub fn sanitize_operation_ids_and_check_duplicate( @@ -93,6 +98,41 @@ pub fn resolve_refs(json: serde_json::Value, root_json: serde_json::Value) -> se } } +pub fn duplicate_payload_schemas( + json: serde_json::Value, + root_json: serde_json::Value, +) -> serde_json::Value { + match json { + serde_json::Value::Object(map) => { + let mut new_map = serde_json::Map::new(); + for (key, value) in map { + if key == "payload" { + if let serde_json::Value::Object(schema) = value { + // insert schema as json string + new_map.insert( + "schema".into(), + serde_json::Value::String(serde_json::to_string(&schema).unwrap()), + ); + new_map.insert("payload".into(), serde_json::Value::Object(schema.clone())); + } + } else { + let new_value = duplicate_payload_schemas(value, root_json.clone()); + new_map.insert(key, new_value); + } + } + serde_json::Value::Object(new_map) + } + serde_json::Value::Array(array) => { + let new_array = array + .into_iter() + .map(|value| duplicate_payload_schemas(value, root_json.clone())) + .collect(); + serde_json::Value::Array(new_array) + } + _ => json, + } +} + pub fn fill_message_and_payload_names( json: serde_json::Value, root_json: serde_json::Value, diff --git a/src/parser/json_schema_parser/array_schema.rs b/src/parser/json_schema_parser/array_schema.rs index be24d84..d7491d2 100644 --- a/src/parser/json_schema_parser/array_schema.rs +++ b/src/parser/json_schema_parser/array_schema.rs @@ -35,6 +35,7 @@ pub fn parse_array_schema( // )) Ok(RustSchemaRepresentation { unique_id: identifyer, + original_key: property_name.to_string(), struct_reference: format!("Vec<{}>", item_type), model_definition: "".to_string(), related_models: vec![], diff --git a/src/parser/json_schema_parser/enum_schema.rs b/src/parser/json_schema_parser/enum_schema.rs index 7240ed7..fa8ebfe 100644 --- a/src/parser/json_schema_parser/enum_schema.rs +++ b/src/parser/json_schema_parser/enum_schema.rs @@ -33,6 +33,7 @@ pub fn parse_enum_schema( string_builder.push_str("}\n"); Ok(RustSchemaRepresentation { unique_id: identifyer.clone(), + original_key: property_name.to_string(), struct_reference: identifyer, model_definition: string_builder, model_type: "enum".to_string(), diff --git a/src/parser/json_schema_parser/object_schema.rs b/src/parser/json_schema_parser/object_schema.rs index a90428f..35d5cf5 100644 --- a/src/parser/json_schema_parser/object_schema.rs +++ b/src/parser/json_schema_parser/object_schema.rs @@ -10,6 +10,7 @@ pub fn parse_object_schema( property_name: &str, ) -> Result { let identifyer = validate_identifier_string(property_name, true); + let before_string: String = format!( "#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct {} {{\n", identifyer @@ -39,13 +40,20 @@ pub fn parse_object_schema( let property_string = unwrapped_property_types .iter() - .map(|x| format!("pub {}: {}", x.unique_id, x.struct_reference)) + .map(|x| { + let rename = match x.original_key == x.unique_id.as_str() { + true => "".to_string(), + false => format!("#[serde(rename = \"{}\")]\n", x.original_key), + }; + format!("{}pub {}: {}", rename, x.unique_id, x.struct_reference) + }) .collect::>() .join(",\n"); let full_struct = before_string + &property_string + &after_string; let representation: RustSchemaRepresentation = RustSchemaRepresentation { unique_id: identifyer.clone(), + original_key: property_name.to_string(), struct_reference: identifyer, model_definition: full_struct, related_models: unwrapped_property_types, diff --git a/src/parser/json_schema_parser/primitive_schema.rs b/src/parser/json_schema_parser/primitive_schema.rs index 216028c..d65c343 100644 --- a/src/parser/json_schema_parser/primitive_schema.rs +++ b/src/parser/json_schema_parser/primitive_schema.rs @@ -21,6 +21,7 @@ pub fn primitive_type_to_string( Ok(RustSchemaRepresentation { unique_id: validate_identifier_string(&variable_name, false), + original_key: property_name.to_string(), struct_reference: format_to_rust_type(&schema_type), model_definition: "".to_string(), related_models: vec![], diff --git a/src/parser/json_schema_parser/types/rust_schema_representation.rs b/src/parser/json_schema_parser/types/rust_schema_representation.rs index b78ec78..7b62a7d 100644 --- a/src/parser/json_schema_parser/types/rust_schema_representation.rs +++ b/src/parser/json_schema_parser/types/rust_schema_representation.rs @@ -4,6 +4,7 @@ use serde::Serialize; pub struct RustSchemaRepresentation { // the unique identifier (e.g. UserSignupMessage) pub unique_id: String, + pub original_key: String, // used to reference the model (e.g. UserSignupMessage, but for primitive schemas simply the primitive type e.g. String/f64) pub struct_reference: String, // model definition (e.g. pub struct UserSignupMessage { ... } or pub enum UserSignupMessage { ... }, is empty for primitive types) diff --git a/src/template_context/types.rs b/src/template_context/types.rs index 1a723a2..406d7d2 100644 --- a/src/template_context/types.rs +++ b/src/template_context/types.rs @@ -44,16 +44,9 @@ pub struct SimplifiedMessage { pub unique_id: String, pub original_message: Message, pub payload: Option, + pub payload_schema: Option, } -// #[derive(Serialize, Debug, Clone)] -// pub struct SimplifiedSchema { -// pub unique_id: String, -// pub original_schema: Schema, -// pub struct_definition: String, -// pub struct_names: Vec, -// // pub multiple_payload_enum: Option, -// } /// FIXME: these are just a quick workaround until gtmpl::Value supports `From for gtmpl::Value` impl<'a> From<&TemplateContext<'a>> for gtmpl::Value { fn from(value: &TemplateContext<'a>) -> Self { @@ -68,6 +61,13 @@ impl From<&SimplifiedOperation> for gtmpl::Value { } } +impl From<&SimplifiedMessage> for gtmpl::Value { + fn from(value: &SimplifiedMessage) -> Self { + let json_value: serde_json::Value = serde_json::to_value(value).unwrap(); + serde_value_to_gtmpl_value(&json_value) + } +} + /// converts any serde serializable value to a gtmpl value /// WARNING: clones objects, so not exactly zero cost abstraction 🤷‍♂️ fn serde_value_to_gtmpl_value(value: &serde_json::Value) -> gtmpl::Value { diff --git a/src/template_context/utilities.rs b/src/template_context/utilities.rs index ef0dfb9..2b794b3 100644 --- a/src/template_context/utilities.rs +++ b/src/template_context/utilities.rs @@ -50,21 +50,13 @@ pub fn simplify_message( let payload = match &message.payload { Some(schema) => { if let Payload::Schema(schema) = schema { - unique_id = validate_identifier_string( - format!( - "{}{}Message", - message.name.as_ref().unwrap_or( - schema - .schema_data - .name - .as_ref() - .unwrap_or(&String::from("")) - ), - unique_parent_id - ) - .as_str(), - false, - ); + let message_name = match &message.name { + Some(name) => name.to_string(), + None => { + format!("{}Message", unique_parent_id) + } + }; + unique_id = validate_identifier_string(&message_name, false); let simplified_schema = simplify_schema(schema, &unique_id); Some(simplified_schema) } else { @@ -77,6 +69,7 @@ pub fn simplify_message( unique_id, original_message: message.clone(), payload, + payload_schema: message.payload_schema.clone(), } } else { panic!("Refs should be resolved by now"); @@ -84,32 +77,9 @@ pub fn simplify_message( } pub fn simplify_schema(schema: &Schema, unique_parent_id: &str) -> RustSchemaRepresentation { - parse_json_schema_to_rust_type(schema, unique_parent_id).unwrap() - // let rust_schema = parse_json_schema_to_rust_type(schema, unique_parent_id).unwrap(); - // let mut schema_source = rust_schema.related_models.clone(); - // schema_source.push(rust_schema.clone()); - // let schemas = schema_source - // .into_iter().map(|s| s.model_definition).collect::>().join("\n"); - // let struct_name =rust_schema.identifier.clone(); - // TODO: this whole thing will need to be refactored, there's no way this will work in this form - // the idea is that we need to get the payload enum and its members out of the schema - // but we save it as string only... so the whole parsing function will need to be restructured and way more modular - // why you ask? we want to automatically generate match code for the payload, but currently it wont work without refactor - - // let payload_enum_name = format!("{}PayloadEnum", unique_parent_id); - // let mut multiple_payload_enum = None; - // if schemas.contains_key(&payload_enum_name) { - // multiple_payload_enum = Some(MultiStructEnum { - // unique_id: payload_enum_name, - // messages: vec![], - // struct_definition: "".to_string(), - // }); - // } - // RustSchemaRepresentation { - // unique_id: unique_parent_id.to_string(), - // original_schema: schema.clone(), - // struct_definition: schemas, - // struct_names: vec![struct_name], - // // multiple_payload_enum: None, - // } + let schema_name = match &schema.schema_data.name { + Some(name) => validate_identifier_string(name, false), + None => validate_identifier_string(unique_parent_id, false), + }; + parse_json_schema_to_rust_type(schema, &schema_name).unwrap() } diff --git a/templates/.env.go b/templates/.env.go index dbf6260..0f544c4 100644 --- a/templates/.env.go +++ b/templates/.env.go @@ -1,10 +1,11 @@ ################General Config################ -SERVICE_PORT = "http://localhost:8080" +SERVICE_PORT = "8080" SERVER_URL = "{{ .server.url }}" LOG_LEVEL = "DEBUG" OPA_RULES= "path/to/admin/policy" TRACING_ENABLED = false +SCHEMA_VALIDATION_ENABLED = true ################Channel wise Config################ {{ range .subscribe_channels }} diff --git a/templates/Cargo.toml.go b/templates/Cargo.toml.go index d7376f5..0b88a60 100644 --- a/templates/Cargo.toml.go +++ b/templates/Cargo.toml.go @@ -24,4 +24,5 @@ opa-wasm = { git = "https://github.com/matrix-org/rust-opa-wasm.git" } cargo_metadata = "0.15.4" warp = "0.3.5" lazy_static = "1.4" +jsonschema = "0.17.0" diff --git a/templates/Readme.md.go b/templates/Readme.md.go index 124a8e6..53a1f0d 100644 --- a/templates/Readme.md.go +++ b/templates/Readme.md.go @@ -13,8 +13,9 @@ Open the documentation with the following command: You can use a cli command to send a message directly on a specified channel for testing purposes. Simply use the following command in the root directory of the generated project: ``` -cargo run -- -c destination/channel -m {myMessageJson} +cargo run -- -c destination/channel -m '{"test": "message"}' ``` +When manually sending messages, please use the property names as they are defined in the specification. Note, to run a second server please change the env variable `SERVICE_PORT` to a different port number. ## Tracing @@ -32,3 +33,10 @@ docker run -d -p6831:6831/udp -p6832:6832/udp -p16686:16686 -p14268:14268 jaeger Access the Jaeger UI at http://localhost:16686 and look for your service name in the dropdown menu. For more information, visit the [Jaeger website](https://www.jaegertracing.io/docs/getting-started/). + +## Validation +The generated microservice uses json schemas for validating the message payload. The schema is the one defined in the specification. Settings like minimum etc. which are supported by json schema can be added there. +The schemas are located in the `schemas` folder. The schema is validated against the message payload in the handler function, you can turn this validation off by changing the SCHEMA_VALIDATION_ENABLED env variable to false. + +Warning: Message validation currently has a high performance cost, so it is recommended to only use it in development. in production the schemas in the generated schema folder could be used to feed a schema registry, which can be used to validate the messages. [asyncapi doc](https://www.asyncapi.com/docs/guides/message-validation#schema-registry-validation) + diff --git a/templates/src/handler/$$handler$$.rs.go b/templates/src/handler/$$handler$$.rs.go index 0b756f9..5e87575 100644 --- a/templates/src/handler/$$handler$$.rs.go +++ b/templates/src/handler/$$handler$$.rs.go @@ -14,7 +14,7 @@ use log::{debug, warn}; /// {{ range .messages }} /// {{ .unique_id }} /// {{ end }} - {{ if key_exists "original_operation" "bindings" "nats" "streamname" }} + {{ if key_exists . "original_operation" "bindings" "nats" "streamname" }} {{ $isStream := .original_operation.bindings.nats.streamname }} {{ end }} @@ -23,7 +23,7 @@ use log::{debug, warn}; pub async fn stream_producer_{{ .unique_id }}(context_stream: &Context, payload : {{ if .payload}} {{ .payload.struct_reference }} {{ else }} () {{ end }}) { //context instead of client let tracer = global::tracer("{{ .unique_id }}_stream_producer"); let _span = tracer.start("stream_producer_{{ .unique_id }}"); - let subject = get_env("{{ .unique_id }}_SUBJECT").unwrap().clone(); + let subject = get_env("{{ $channel.unique_id }}_SUBJECT").unwrap().clone(); {{ if .payload }} let payload = match serde_json::to_string(&payload) { Ok(payload) => payload, @@ -43,7 +43,7 @@ use log::{debug, warn}; pub async fn producer_{{ .unique_id }}(client: &Client, payload: {{ if .payload }} {{.payload.struct_reference}} {{else}} () {{end}}) { let tracer = global::tracer("{{ .unique_id }}_producer"); let _span = tracer.start("producer_{{ .unique_id }}"); - let subject = get_env("{{ .unique_id }}_SUBJECT").unwrap().clone(); + let subject = get_env("{{ $channel.unique_id }}_SUBJECT").unwrap().clone(); {{ if .payload }} let payload = match serde_json::to_string(&payload) { Ok(payload) => payload, diff --git a/templates/src/handler/$$producer$$.rs.go b/templates/src/handler/$$producer$$.rs.go index 68f5d78..a99ad2f 100644 --- a/templates/src/handler/$$producer$$.rs.go +++ b/templates/src/handler/$$producer$$.rs.go @@ -1,10 +1,10 @@ use async_nats::{Client, Message, jetstream}; use async_nats::jetstream::Context; -use crate::{publish_message,stream_publish_message,model::*,config::*,policy::*}; -use std::time; +use crate::{publish_message,stream_publish_message,model::*,config::*,policy::policy::*, utils::*}; +use std::{time, path::Path}; use opentelemetry::global; use opentelemetry::trace::Tracer; -use log::{debug, warn}; +use log::{debug, warn, error}; {{ $isStream := false }} @@ -13,7 +13,7 @@ use log::{debug, warn}; /// {{ range .messages }} /// {{ .unique_id }} /// {{ end }} - {{if key_exists "original_operation" "bindings" "nats" "streamname"}} + {{if key_exists . "original_operation" "bindings" "nats" "streamname"}} {{ $isStream := (.original_operation.bindings.nats.streamname) }} {{end}} {{if $isStream}} @@ -22,7 +22,23 @@ use log::{debug, warn}; let _span = tracer.start("{{ .unique_id }}_stream_handler"); {{ range .messages }} {{ if .payload}} - match serde_json::from_slice::<{{ .payload.struct_reference }}>(&message.message.payload.as_ref()) { + let payload = match serde_json::from_slice::(&message.message.payload) { + Ok(payload) => payload, + Err(_) => { + error!("Failed to deserialize message payload, make sure payload is a valid json: user_signed_up_message\nOriginal message: {:#?}", message); + return; + } + }; + {{ if .payload_schema}} + if let Err(e) =validate_message_schema( + Path::new("./src/schemas/{{ .unique_id }}_payload_schema.json"), + &payload, + ) { + error!("Failed to validate message schema: user_signed_up_message\nOriginal message: {:#?}\nError: {}", message, e); + return; + } + {{ end }} + match serde_json::from_value::<{{ .payload.struct_reference }}>(payload) { Ok(deserialized_message) => { debug!("Received message {:#?}", deserialized_message); let policy_reply = opa_eval(&deserialized_message); @@ -49,11 +65,27 @@ use log::{debug, warn}; } {{else}} pub async fn handler_{{ .unique_id }}(message: Message, client: &Client) { - let tracer = global::tracer("handler_{{ .unique_id }}"); + let tracer = global::tracer("handler_{{ .unique_id }}"); let _span = tracer.start("{{ .unique_id }}_handler"); - {{ range .messages }} + {{ range .messages }} {{ if .payload}} - match serde_json::from_slice::<{{ .payload.struct_reference }}>(&message.payload.as_ref()) { + let payload = match serde_json::from_slice::(&message.payload) { + Ok(payload) => payload, + Err(_) => { + error!("Failed to deserialize message payload, make sure payload is a valid json: user_signed_up_message\nOriginal message: {:#?}", message); + return; + } + }; + {{ if .payload_schema}} + if let Err(e) =validate_message_schema( + Path::new("./src/schemas/{{ .unique_id }}_payload_schema.json"), + &payload, + ) { + error!("Failed to validate message schema: user_signed_up_message\nOriginal message: {:#?}\nError: {}", message, e); + return; + } + {{ end }} + match serde_json::from_value::<{{ .payload.struct_reference }}>(payload) { Ok(deserialized_message) => { let policy_reply = opa_eval(&deserialized_message); {{ if eq .payload.model_type "enum"}} diff --git a/templates/src/policy/mod.rs.go b/templates/src/policy/mod.rs.go index 877f7a6..32156d6 100644 --- a/templates/src/policy/mod.rs.go +++ b/templates/src/policy/mod.rs.go @@ -1,2 +1,2 @@ pub mod policy; -pub use policy::*; +use policy::*; \ No newline at end of file diff --git a/templates/src/schemas/$$schemas$$.json.go b/templates/src/schemas/$$schemas$$.json.go new file mode 100644 index 0000000..d4cf4a4 --- /dev/null +++ b/templates/src/schemas/$$schemas$$.json.go @@ -0,0 +1 @@ +{{ .payload_schema }} \ No newline at end of file diff --git a/templates/src/utils/mod.rs.go b/templates/src/utils/mod.rs.go index e16f31a..a47b549 100644 --- a/templates/src/utils/mod.rs.go +++ b/templates/src/utils/mod.rs.go @@ -1,4 +1,6 @@ pub mod common; pub use common::*; pub mod streams; -pub use streams::*; \ No newline at end of file +pub use streams::*; +pub mod validator; +pub use validator::*; \ No newline at end of file diff --git a/templates/src/utils/validator.rs.go b/templates/src/utils/validator.rs.go new file mode 100644 index 0000000..efae84c --- /dev/null +++ b/templates/src/utils/validator.rs.go @@ -0,0 +1,34 @@ +use jsonschema::JSONSchema; + +pub fn validate_message_schema( + validator_path: &std::path::Path, + instance: &serde_json::Value, +) -> Result<(), String> { + match std::env::var("SCHEMA_VALIDATION_ENABLED") { + Ok(enabled) => { + if enabled == "false" { + return Ok(()); + } + } + Err(_) => return Ok(()), + }; + // read json schema file as json value + let schema_source = match std::fs::read(validator_path){ + Ok(schema) => schema, + Err(_) => return Err("❌ Failed to read schema file in path ".to_string() + validator_path.to_str().unwrap()), + }; + let schema = match serde_json::from_slice::(&schema_source){ + Ok(schema) => schema, + Err(_) => return Err("❌ Failed to parse schema file in path ".to_string() + validator_path.to_str().unwrap()), + }; + let compiled = match JSONSchema::compile(&schema){ + Ok(compiled) => compiled, + Err(_) => return Err("❌ Failed to compile schema in path".to_string() + validator_path.to_str().unwrap()), + }; + let result = compiled.validate(instance); + if let Err(errors) = result { + Err("❌ Message payload invalid!, errors: ".to_string() + &errors.map(|e| e.to_string()).collect::>().join(", ")) + } else { + Ok(()) + } +}