From 3363433c39548227453075d1814a8fd3d18c9ed2 Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Sat, 9 Mar 2024 12:55:27 -0800 Subject: [PATCH 1/8] feature(generator): add jq-like filter support to make artifact generation more flexible --- Cargo.lock | 105 ++++++++++++ crates/weaver_forge/Cargo.toml | 7 +- crates/weaver_forge/src/config.rs | 62 +++++++ crates/weaver_forge/src/filter.rs | 113 +++++++++++++ crates/weaver_forge/src/lib.rs | 156 +++++++++++++++--- src/registry/generate.rs | 14 +- templates/registry/markdown/attribute_type.j2 | 5 + templates/registry/markdown/examples.j2 | 7 + templates/registry/markdown/group.md | 54 ++++++ templates/registry/markdown/groups.md | 47 ++++++ templates/registry/markdown/registry.md | 14 +- templates/registry/markdown/weaver.yaml | 10 ++ 12 files changed, 552 insertions(+), 42 deletions(-) create mode 100644 crates/weaver_forge/src/filter.rs create mode 100644 templates/registry/markdown/attribute_type.j2 create mode 100644 templates/registry/markdown/examples.j2 create mode 100644 templates/registry/markdown/group.md create mode 100644 templates/registry/markdown/groups.md create mode 100644 templates/registry/markdown/weaver.yaml diff --git a/Cargo.lock b/Cargo.lock index 09c361a2..03af8280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -157,6 +158,15 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -302,6 +312,15 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown", +] + [[package]] name = "clap" version = "4.5.2" @@ -601,6 +620,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.10.0" @@ -1570,6 +1595,7 @@ dependencies = [ "log", "regex-automata 0.4.6", "regex-syntax 0.8.2", + "serde", ] [[package]] @@ -1624,6 +1650,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hifijson" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ef6b41c333e6dd2a4aaa59125a19b633cd17e7aaf372b2260809777bcdef4a" + [[package]] name = "home" version = "0.5.9" @@ -1849,6 +1881,68 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jaq-core" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d6a5713b8f33675abfac79d1db0022a3f28764b2a6b96a185c199ad8dab86d" +dependencies = [ + "aho-corasick", + "base64", + "hifijson", + "jaq-interpret", + "libm", + "log", + "regex", + "time", + "urlencoding", +] + +[[package]] +name = "jaq-interpret" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569e38e5fc677db8dfda89ee0b4c25b3f53e811b16434fd14bdc5b43fc362ac" +dependencies = [ + "ahash", + "dyn-clone", + "hifijson", + "indexmap", + "jaq-syn", + "once_cell", + "serde_json", +] + +[[package]] +name = "jaq-parse" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6f8beb9f9922546419e774e24199e8a968f54c63a5a2323c8f3ef3321ace14" +dependencies = [ + "chumsky", + "jaq-syn", +] + +[[package]] +name = "jaq-std" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7871c59297cbfdd18f6f1bbbafaad24e97fd555ee1e2a1be7a40a5a20f551a" +dependencies = [ + "bincode", + "jaq-parse", + "jaq-syn", +] + +[[package]] +name = "jaq-syn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d60101fb791b20c982731d848ed6e7d25363656497647c2093b68bd88398d6" +dependencies = [ + "serde", +] + [[package]] name = "jobserver" version = "0.1.28" @@ -3655,6 +3749,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8-ranges" version = "1.0.5" @@ -3846,6 +3946,11 @@ version = "0.1.0" dependencies = [ "convert_case", "glob", + "globset", + "jaq-core", + "jaq-interpret", + "jaq-parse", + "jaq-std", "minijinja", "rayon", "serde", diff --git a/crates/weaver_forge/Cargo.toml b/crates/weaver_forge/Cargo.toml index 733a8bd6..016f93a9 100644 --- a/crates/weaver_forge/Cargo.toml +++ b/crates/weaver_forge/Cargo.toml @@ -15,7 +15,12 @@ weaver_semconv = { path = "../weaver_semconv" } minijinja = { version = "1.0.12", features = ["loader", "custom_syntax"] } convert_case = "0.6.0" -glob = "0.3.1" +glob = "0.3.1" # ToDo: Should we still use this as globset is used anyway? +globset = { version = "0.4.14", features = ["serde1"] } +jaq-core = "1.2.1" +jaq-std = "1.2.1" +jaq-interpret = "1.2.1" +jaq-parse = "1.0.2" thiserror.workspace = true serde.workspace = true diff --git a/crates/weaver_forge/src/config.rs b/crates/weaver_forge/src/config.rs index f21209f2..b08161a9 100644 --- a/crates/weaver_forge/src/config.rs +++ b/crates/weaver_forge/src/config.rs @@ -6,10 +6,12 @@ use std::collections::HashMap; use std::path::Path; use convert_case::{Case, Casing}; +use globset::{Glob, GlobSet, GlobSetBuilder}; use serde::Deserialize; use crate::Error; use crate::Error::InvalidConfigFile; +use crate::filter::Filter; /// Case convention for naming of functions and structs. #[derive(Deserialize, Clone, Debug)] @@ -57,6 +59,50 @@ pub struct TargetConfig { /// Configuration for the template syntax. #[serde(default)] pub template_syntax: TemplateSyntax, + + /// Configuration for the templates. + #[serde(default)] + pub templates: Vec, +} + +/// Application mode defining how to apply a template on the result of a +/// filter applied on a registry. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ApplicationMode { + /// Apply the template to the output of the filter as a whole. + Single, + /// Apply the template to each item of the list returned by the filter. + Each, +} + +/// A template configuration. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub struct TemplateConfig { + /// The pattern used to identify when this template configuration must be + /// applied to a specific template file. + pub pattern: Glob, + /// The filter to apply to the registry before applying the template. + /// Applying a filter to a registry will return a list of elements from the + /// registry that satisfy the filter. + pub filter: Filter, + /// The mode to apply the template. + /// `single`: Apply the template to the output of the filter as a whole. + /// `each`: Apply the template to each item of the list returned by the filter. + pub application_mode: ApplicationMode, +} + +/// A template matcher. +pub struct TemplateMatcher<'a> { + templates: &'a [TemplateConfig], + glob_set: GlobSet, +} + +impl <'a> TemplateMatcher<'a> { + pub fn matches>(&self, path: P) -> Vec<&'a TemplateConfig> { + self.glob_set.matches(path).into_iter().map(|i| &self.templates[i]).collect() + } } /// Syntax configuration for the template engine. @@ -178,4 +224,20 @@ impl TargetConfig { Ok(TargetConfig::default()) } } + + /// Return a template matcher for the target configuration. + pub fn template_matcher(&self) -> Result { + let mut builder = GlobSetBuilder::new(); + + self.templates.iter().for_each(|template| { + _ = builder.add(template.pattern.clone()); + }); + + builder.build().map_err(|e| Error::InvalidTemplatePattern { + error: e.to_string(), + }).map(|glob_set| TemplateMatcher { + templates: &self.templates, + glob_set, + }) + } } diff --git a/crates/weaver_forge/src/filter.rs b/crates/weaver_forge/src/filter.rs new file mode 100644 index 00000000..261426df --- /dev/null +++ b/crates/weaver_forge/src/filter.rs @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Filter JSON values using a simple expression language. + +use core::fmt; +use std::fmt::Debug; +use jaq_interpret::{Ctx, FilterT, RcIter, Val}; +use serde::de; +use crate::Error; + +/// A filter that can be applied to a JSON value. +pub struct Filter { + filter_expr: String, + filter: jaq_interpret::Filter, +} + +impl Filter { + /// Create a new filter from a string expression or return an error if the + /// expression is invalid. + pub fn try_new(filter_expr: &str) -> Result { + let vars = Vec::new(); + let mut ctx = jaq_interpret::ParseCtx::new(vars); + ctx.insert_natives(jaq_core::core()); + ctx.insert_defs(jaq_std::std()); + + let (parsed_expr, errs) = jaq_parse::parse(filter_expr, jaq_parse::main()); + + // If there are any errors, return them + if !errs.is_empty() { + return Err(Error::CompoundError(errs.into_iter().map(|e| Error::FilterError { + filter: filter_expr.to_string(), + error: e.to_string() + }).collect())); + } + + let parsed_expr = parsed_expr.ok_or_else(|| Error::FilterError { + filter: filter_expr.to_string(), + error: "No parsed expression".to_string() + })?; + + Ok(Self { + filter_expr: filter_expr.to_string(), + filter: ctx.compile(parsed_expr), + }) + } + + /// Apply the filter to a JSON value and return the result as a JSON value. + pub fn apply(&self, json: serde_json::Value) -> Result { + let inputs = RcIter::new(core::iter::empty()); + let mut filter_result = self.filter.run((Ctx::new([], &inputs), Val::from(json))); + let mut errs = Vec::new(); + let mut values = Vec::new(); + + while let Some(r) = filter_result.next() { + match r { + Ok(v) => values.push(serde_json::Value::from(v)), + Err(e) => errs.push(e), + } + } + + if values.len() == 1 { + return Ok(values.pop().unwrap()); + } + + Ok(serde_json::Value::Array(values)) + } +} + +impl Debug for Filter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Filter({})", self.filter_expr) + } +} + +struct FilterVisitor; + +impl<'de> de::Visitor<'de> for FilterVisitor { + type Value = Filter; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a filter string") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Filter::try_new(value).map_err(E::custom) + } +} + +impl<'de> de::Deserialize<'de> for Filter { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + deserializer.deserialize_str(FilterVisitor) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use crate::filter::Filter; + + #[test] + fn test_jaq() -> Result<(), crate::Error> { + let filter = Filter::try_new(".b").unwrap(); + let json = json!({"a": 1, "b": {"c": 1, "d": 2}}); + assert_eq!(filter.apply(json)?, json!([{"c": 1, "d": 2}])); + Ok(()) + } +} \ No newline at end of file diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 08d00ef1..4baf2aef 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -5,26 +5,27 @@ //! schemas. #![deny( - missing_docs, - clippy::print_stdout, - unstable_features, - unused_import_braces, - unused_qualifications, - unused_results, - unused_extern_crates +missing_docs, +clippy::print_stdout, +unstable_features, +unused_import_braces, +unused_qualifications, +unused_results, +unused_extern_crates )] use std::fmt::{Debug, Display, Formatter}; -use std::fs; +use std::{clone, fs}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use glob::{glob, Paths}; +use minijinja::{Environment, path_loader, State, Value}; use minijinja::value::{from_args, Object}; -use minijinja::{path_loader, Environment, State, Value}; use rayon::iter::IntoParallelIterator; use rayon::iter::ParallelIterator; use serde::Serialize; +use walkdir::{DirEntry, WalkDir}; use weaver_logger::Logger; use weaver_resolved_schema::attribute::AttributeRef; @@ -32,17 +33,15 @@ use weaver_resolved_schema::catalog::Catalog; use weaver_resolved_schema::registry::Registry; use weaver_semconv::group::GroupType; -use crate::config::TargetConfig; +use crate::config::{ApplicationMode, TargetConfig}; +use crate::Error::{CompoundError, InternalError, InvalidTemplateDir, InvalidTemplateDirectory, InvalidTemplateFile, TargetNotSupported, WriteGeneratedCodeFailed}; use crate::extensions::case_converter::case_converter; use crate::registry::{TemplateGroup, TemplateRegistry}; -use crate::Error::{ - InternalError, InvalidTemplateDir, InvalidTemplateDirectory, InvalidTemplateFile, - TargetNotSupported, WriteGeneratedCodeFailed, -}; mod config; mod extensions; mod registry; +mod filter; /// Errors emitted by this crate. #[derive(thiserror::Error, Debug)] @@ -127,11 +126,37 @@ pub enum Error { attr_ref: AttributeRef, }, + /// Filter error. + #[error("Filter '{filter}' failed: {error}")] + FilterError { + /// Filter that caused the error. + filter: String, + /// Error message. + error: String, + }, + + /// Invalid template pattern. + #[error("Invalid template pattern: {error}")] + InvalidTemplatePattern { + /// Error message. + error: String, + }, + /// A generic container for multiple errors. #[error("Errors:\n{0:#?}")] CompoundError(Vec), } +/// Handles a list of errors and returns a compound error if the list is not +/// empty or () if the list is empty. +pub fn handle_errors(errors: Vec) -> Result<(), Error> { + if errors.is_empty() { + Ok(()) + } else { + Err(CompoundError(errors)) + } +} + /// General configuration for the generator. pub struct GeneratorConfig { /// Root directory for the templates. @@ -194,7 +219,7 @@ impl Object for TemplateObject { args: &[Value], ) -> Result { if name == "set_file_name" { - let (file_name,): (&str,) = from_args(args)?; + let (file_name, ): (&str, ) = from_args(args)?; *self.file_name.lock().unwrap() = file_name.to_string(); Ok(Value::from("")) } else { @@ -227,6 +252,13 @@ pub struct Context<'a> { pub groups: Option>, } +/// Global context for the template engine. +#[derive(Serialize, Debug)] +pub struct NewContext<'a> { + /// The semantic convention registry. + pub ctx: &'a serde_json::Value, +} + impl TemplateEngine { /// Create a new template engine for the given target or return an error if /// the target does not exist or is not a directory. @@ -251,6 +283,76 @@ impl TemplateEngine { // ToDo Refactor InternalError // ToDo Use compound error + /// Generate artifacts from the template directory, in parallel. + pub fn generate( + &self, + log: impl Logger + Clone + Sync, + registry: &Registry, + catalog: &Catalog, + output_dir: &Path, + ) -> Result<(), Error> { + // List all files in the target directory and its subdirectories + let files: Vec = WalkDir::new(self.path.clone()).into_iter().filter_map(|e| { + // Skip directories that the owner of the running process does not + // have permission to access + e.ok() + }).filter(|dir_entry| dir_entry.path().is_file()).collect(); + + let config = TargetConfig::try_new(&self.path)?; + let tmpl_matcher = config.template_matcher()?; + + let template_registry = TemplateRegistry::try_from_resolved_registry(registry, catalog) + .map_err(|e| InternalError(e.to_string()))?; + let template_registry = serde_json::to_value(template_registry).map_err(|e| InternalError(e.to_string()))?; + + let errs = files.into_par_iter().filter_map(|file| { + let relative_path = match file.path().strip_prefix(&self.path) { + Ok(relative_path) => relative_path, + Err(e) => return Some(InvalidTemplateDir { + template_dir: self.path.clone(), + error: e.to_string(), + }), + }; + + for template in tmpl_matcher.matches(relative_path) { + let filtered_result = match template.filter.apply(template_registry.clone()) { + Ok(result) => result, + Err(e) => return Some(e), + }; + + match template.application_mode { + // The filtered result is evaluated as a single object + ApplicationMode::Single => { + if let Err(e) = self.evaluate_template(log.clone(), serde_json::to_value(NewContext {ctx: &filtered_result }).unwrap(), relative_path, output_dir) { + return Some(e); + } + } + // The filtered result is evaluated as an array of objects + // and each object is evaluated independently and in parallel + // with the same template. + ApplicationMode::Each => { + if let Some(values) = filtered_result.as_array() { + let errs = values.into_par_iter().filter_map(|result| { + if let Err(e) = self.evaluate_template(log.clone(), serde_json::to_value(NewContext {ctx: &result}).unwrap(), relative_path, output_dir) { + return Some(e); + } + None + }).collect::>(); + if errs.len() > 0 { + return Some(CompoundError(errs)); + } + } else if let Err(e) = self.evaluate_template(log.clone(), serde_json::to_value(NewContext {ctx: &filtered_result }).unwrap(), relative_path, output_dir) { + return Some(e); + } + } + } + } + None + }).collect::>(); + + handle_errors(errs) + } + /// Generate assets from a semantic convention registry. pub fn generate_registry( &self, @@ -285,8 +387,8 @@ impl TemplateEngine { group: Some(group), groups: None, }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path, output_dir) + .map_err(|e| InternalError(e.to_string()))?; + self.evaluate_template(log.clone(), ctx, relative_template_path.as_path(), output_dir) } TemplateObjectPair::Groups { template_path: relative_template_path, @@ -297,8 +399,8 @@ impl TemplateEngine { group: None, groups: Some(groups), }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path, output_dir) + .map_err(|e| InternalError(e.to_string()))?; + self.evaluate_template(log.clone(), ctx, relative_template_path.as_path(), output_dir) } TemplateObjectPair::Registry { template_path: relative_template_path, @@ -309,8 +411,8 @@ impl TemplateEngine { group: None, groups: None, }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path, output_dir) + .map_err(|e| InternalError(e.to_string()))?; + self.evaluate_template(log.clone(), ctx, relative_template_path.as_path(), output_dir) } })?; @@ -321,7 +423,7 @@ impl TemplateEngine { &self, log: impl Logger + Clone + Sync, ctx: serde_json::Value, - template_path: PathBuf, + template_path: &Path, output_dir: &Path, ) -> Result<(), Error> { let template_object = TemplateObject { @@ -331,7 +433,7 @@ impl TemplateEngine { }; let mut engine = self.template_engine()?; let template_file = template_path.to_str().ok_or(InvalidTemplateFile { - template: template_path.clone(), + template: template_path.to_path_buf(), error: "".to_string(), })?; @@ -668,7 +770,9 @@ mod tests { use std::collections::HashSet; use std::fs; use std::path::Path; + use walkdir::WalkDir; + use weaver_logger::TestLogger; use weaver_resolver::SchemaResolver; use weaver_semconv::SemConvRegistry; @@ -750,9 +854,9 @@ mod tests { .collect::>() .is_empty() || !observed_files - .difference(&expected_files) - .collect::>() - .is_empty() + .difference(&expected_files) + .collect::>() + .is_empty() { are_identical = false; } diff --git a/src/registry/generate.rs b/src/registry/generate.rs index 97ab55b0..16db8b54 100644 --- a/src/registry/generate.rs +++ b/src/registry/generate.rs @@ -73,14 +73,12 @@ pub(crate) fn command( ) .expect("Failed to create template engine"); - engine - .generate_registry( - logger.clone(), - &schema.registries[0], - &schema.catalog, - args.output.as_path(), - ) - .expect("Failed to generate registry assets"); + engine.generate( + logger.clone(), + &schema.registries[0], + &schema.catalog, + args.output.as_path(), + ).expect("Failed to generate artifacts"); logger.success("Artifacts generated successfully"); } diff --git a/templates/registry/markdown/attribute_type.j2 b/templates/registry/markdown/attribute_type.j2 new file mode 100644 index 00000000..1c9147ba --- /dev/null +++ b/templates/registry/markdown/attribute_type.j2 @@ -0,0 +1,5 @@ +{%- if attribute.type is mapping %} +- Type: Enum [{{ attribute.type.members | map(attribute="value") | join(", ") }}] +{%- else %} +- Type: {{ attribute.type }} +{%- endif %} \ No newline at end of file diff --git a/templates/registry/markdown/examples.j2 b/templates/registry/markdown/examples.j2 new file mode 100644 index 00000000..337fc479 --- /dev/null +++ b/templates/registry/markdown/examples.j2 @@ -0,0 +1,7 @@ +{%- if attribute.examples %} +{%- if attribute.examples is sequence %} +- Examples: {{ attribute.examples | pprint }} +{%- else %} +- Examples: {{ attribute.examples }} +{%- endif %} +{%- endif %} diff --git a/templates/registry/markdown/group.md b/templates/registry/markdown/group.md new file mode 100644 index 00000000..802ccb77 --- /dev/null +++ b/templates/registry/markdown/group.md @@ -0,0 +1,54 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("group/" ~ file_name ~ ".md") -}} + +# Group `{{ ctx.id }}` ({{ ctx.type }}) + +## Brief + +{{ ctx.brief | trim }} + +prefix: {{ ctx.prefix }} + +## Attributes + +{% for attribute in ctx.attributes %} +### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required +{%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} +{%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended +{%- else %} +- Requirement Level: Optional +{%- endif %} +{% if attribute.tag %} +- Tag: {{ attribute.tag }} +{% endif %} +{%- include "attribute_type.j2" %} +{%- include "examples.j2" -%} +{%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} +{%- endif %} +{%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} +{%- endif %} +{% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} +{% endif %} +{% endfor %} + +## Provenance + +Source: {{ ctx.lineage.provenance }} + +{% for item in ctx.lineage.attributes -%} +item: {{ ctx.lineage.attributes[item] }} +{% endfor -%} \ No newline at end of file diff --git a/templates/registry/markdown/groups.md b/templates/registry/markdown/groups.md new file mode 100644 index 00000000..8b072179 --- /dev/null +++ b/templates/registry/markdown/groups.md @@ -0,0 +1,47 @@ +# Semantic Convention Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +prefix: {{ group.prefix }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} +{% endfor %} diff --git a/templates/registry/markdown/registry.md b/templates/registry/markdown/registry.md index e1c26c56..3bd4b2c5 100644 --- a/templates/registry/markdown/registry.md +++ b/templates/registry/markdown/registry.md @@ -3,49 +3,49 @@ Url: {{ registry_url }} # Attribute Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "attribute_group" %} - [{{ group.id }}](attribute_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Events -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "event" %} - [{{ group.id }}](event/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metrics -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric" %} - [{{ group.id }}](metric/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metric Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric_group" %} - [{{ group.id }}](metric_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Resource -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "resource" %} - [{{ group.id }}](resource/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Scope -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "scope" %} - [{{ group.id }}](scope/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Span -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "span" %} - [{{ group.id }}](span/{{ group.id | file_name }}.md) {%- endif %} diff --git a/templates/registry/markdown/weaver.yaml b/templates/registry/markdown/weaver.yaml new file mode 100644 index 00000000..75ab8b19 --- /dev/null +++ b/templates/registry/markdown/weaver.yaml @@ -0,0 +1,10 @@ +templates: + - pattern: registry.md + filter: . + application_mode: single + - pattern: group.md + filter: .groups + application_mode: each + - pattern: groups.md + filter: .groups + application_mode: single \ No newline at end of file From b4403c6c881a15d19cd7b853f8208806bf5ea1e9 Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Sat, 9 Mar 2024 13:33:46 -0800 Subject: [PATCH 2/8] feature(generator): add support for all group types --- crates/weaver_forge/src/lib.rs | 30 +++++++---- .../templates/test/attribute_group.md | 10 ++-- crates/weaver_forge/templates/test/event.md | 2 +- src/registry/generate.rs | 7 ++- .../registry/markdown/attribute_group.md | 46 ++++++++++++++++ .../registry/markdown/attribute_groups.md | 47 ++++++++++++++++ templates/registry/markdown/event.md | 47 ++++++++++++++++ templates/registry/markdown/events.md | 48 +++++++++++++++++ templates/registry/markdown/metric.md | 52 ++++++++++++++++++ templates/registry/markdown/metrics.md | 53 +++++++++++++++++++ templates/registry/markdown/resource.md | 46 ++++++++++++++++ templates/registry/markdown/resources.md | 47 ++++++++++++++++ templates/registry/markdown/scope.md | 0 templates/registry/markdown/span.md | 49 +++++++++++++++++ templates/registry/markdown/spans.md | 50 +++++++++++++++++ templates/registry/markdown/weaver.yaml | 32 ++++++++++- 16 files changed, 546 insertions(+), 20 deletions(-) create mode 100644 templates/registry/markdown/attribute_group.md create mode 100644 templates/registry/markdown/attribute_groups.md create mode 100644 templates/registry/markdown/event.md create mode 100644 templates/registry/markdown/events.md create mode 100644 templates/registry/markdown/metric.md create mode 100644 templates/registry/markdown/metrics.md create mode 100644 templates/registry/markdown/resource.md create mode 100644 templates/registry/markdown/resources.md create mode 100644 templates/registry/markdown/scope.md create mode 100644 templates/registry/markdown/span.md create mode 100644 templates/registry/markdown/spans.md diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 4baf2aef..5311ff39 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -15,7 +15,7 @@ unused_extern_crates )] use std::fmt::{Debug, Display, Formatter}; -use std::{clone, fs}; +use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; @@ -40,7 +40,7 @@ use crate::registry::{TemplateGroup, TemplateRegistry}; mod config; mod extensions; -mod registry; +pub mod registry; mod filter; /// Errors emitted by this crate. @@ -281,14 +281,24 @@ impl TemplateEngine { } // ToDo Refactor InternalError - // ToDo Use compound error - /// Generate artifacts from the template directory, in parallel. - pub fn generate( + /// Generate artifacts from a serializable context and a template directory, + /// in parallel. + /// + /// # Arguments + /// + /// * `log` - The logger to use for logging. + /// * `context` - The context to use for generating the artifacts. + /// * `output_dir` - The directory where the generated artifacts will be saved. + /// + /// # Returns + /// + /// * `Ok(())` if the artifacts were generated successfully. + /// * `Err(error)` if an error occurred during the generation of the artifacts. + pub fn generate( &self, log: impl Logger + Clone + Sync, - registry: &Registry, - catalog: &Catalog, + context: &T, output_dir: &Path, ) -> Result<(), Error> { // List all files in the target directory and its subdirectories @@ -301,9 +311,7 @@ impl TemplateEngine { let config = TargetConfig::try_new(&self.path)?; let tmpl_matcher = config.template_matcher()?; - let template_registry = TemplateRegistry::try_from_resolved_registry(registry, catalog) - .map_err(|e| InternalError(e.to_string()))?; - let template_registry = serde_json::to_value(template_registry).map_err(|e| InternalError(e.to_string()))?; + let context = serde_json::to_value(context).map_err(|e| InternalError(e.to_string()))?; let errs = files.into_par_iter().filter_map(|file| { let relative_path = match file.path().strip_prefix(&self.path) { @@ -315,7 +323,7 @@ impl TemplateEngine { }; for template in tmpl_matcher.matches(relative_path) { - let filtered_result = match template.filter.apply(template_registry.clone()) { + let filtered_result = match template.filter.apply(context.clone()) { Ok(result) => result, Err(e) => return Some(e), }; diff --git a/crates/weaver_forge/templates/test/attribute_group.md b/crates/weaver_forge/templates/test/attribute_group.md index 24c5023d..1fed8404 100644 --- a/crates/weaver_forge/templates/test/attribute_group.md +++ b/crates/weaver_forge/templates/test/attribute_group.md @@ -1,17 +1,17 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("attribute_group/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -prefix: {{ group.prefix }} +prefix: {{ ctx.prefix }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/event.md b/crates/weaver_forge/templates/test/event.md index 2752b07f..5ba06ad0 100644 --- a/crates/weaver_forge/templates/test/event.md +++ b/crates/weaver_forge/templates/test/event.md @@ -1,4 +1,4 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.group.id | file_name -%} {{- template.set_file_name("event/" ~ file_name ~ ".md") -}} # Group `{{ group.id }}` ({{ group.type }}) diff --git a/src/registry/generate.rs b/src/registry/generate.rs index 16db8b54..f7ab99bc 100644 --- a/src/registry/generate.rs +++ b/src/registry/generate.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use weaver_cache::Cache; use weaver_forge::{GeneratorConfig, TemplateEngine}; +use weaver_forge::registry::TemplateRegistry; use weaver_logger::Logger; use weaver_resolver::SchemaResolver; @@ -73,10 +74,12 @@ pub(crate) fn command( ) .expect("Failed to create template engine"); + let template_registry = TemplateRegistry::try_from_resolved_registry(&schema.registries[0], &schema.catalog) + .unwrap_or_else(|e| panic!("Failed to create the context for the template evaluation: {:?}", e)); + engine.generate( logger.clone(), - &schema.registries[0], - &schema.catalog, + &template_registry, args.output.as_path(), ).expect("Failed to generate artifacts"); diff --git a/templates/registry/markdown/attribute_group.md b/templates/registry/markdown/attribute_group.md new file mode 100644 index 00000000..1fed8404 --- /dev/null +++ b/templates/registry/markdown/attribute_group.md @@ -0,0 +1,46 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("attribute_group/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +prefix: {{ ctx.prefix }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/templates/registry/markdown/attribute_groups.md b/templates/registry/markdown/attribute_groups.md new file mode 100644 index 00000000..5cd2168b --- /dev/null +++ b/templates/registry/markdown/attribute_groups.md @@ -0,0 +1,47 @@ +# Semantic Convention Attribute Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +prefix: {{ group.prefix }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} diff --git a/templates/registry/markdown/event.md b/templates/registry/markdown/event.md new file mode 100644 index 00000000..43736aa7 --- /dev/null +++ b/templates/registry/markdown/event.md @@ -0,0 +1,47 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("event/" ~ file_name ~ ".md") -}} + +# Group `{{ ctx.id }}` ({{ ctx.type }}) + +## Brief + +{{ ctx.brief | trim }} + +Prefix: {{ ctx.prefix }} +Name: {{ ctx.name }} + +## Attributes + +{% for attribute in ctx.attributes %} +### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/templates/registry/markdown/events.md b/templates/registry/markdown/events.md new file mode 100644 index 00000000..eae78bc1 --- /dev/null +++ b/templates/registry/markdown/events.md @@ -0,0 +1,48 @@ +# Semantic Convention Event Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +Prefix: {{ group.prefix }} +Name: {{ group.name }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} diff --git a/templates/registry/markdown/metric.md b/templates/registry/markdown/metric.md new file mode 100644 index 00000000..d45df277 --- /dev/null +++ b/templates/registry/markdown/metric.md @@ -0,0 +1,52 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("metric/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +{{ ctx.note | trim }} + +Prefix: {{ ctx.prefix }} +Metric: {{ ctx.metric_name }} +Instrument: {{ ctx.instrument }} +Unit: {{ ctx.unit }} +Stability: {{ ctx.stability | capitalize }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/templates/registry/markdown/metrics.md b/templates/registry/markdown/metrics.md new file mode 100644 index 00000000..641805cb --- /dev/null +++ b/templates/registry/markdown/metrics.md @@ -0,0 +1,53 @@ +# Semantic Convention Metric Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +{{ group.note | trim }} + +Prefix: {{ group.prefix }} +Metric: {{ group.metric_name }} +Instrument: {{ group.instrument }} +Unit: {{ group.unit }} +Stability: {{ group.stability | capitalize }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} diff --git a/templates/registry/markdown/resource.md b/templates/registry/markdown/resource.md new file mode 100644 index 00000000..5fb7efa6 --- /dev/null +++ b/templates/registry/markdown/resource.md @@ -0,0 +1,46 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("resource/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +prefix: {{ ctx.prefix }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} diff --git a/templates/registry/markdown/resources.md b/templates/registry/markdown/resources.md new file mode 100644 index 00000000..955b76fc --- /dev/null +++ b/templates/registry/markdown/resources.md @@ -0,0 +1,47 @@ +# Semantic Convention Resource Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +prefix: {{ group.prefix }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} \ No newline at end of file diff --git a/templates/registry/markdown/scope.md b/templates/registry/markdown/scope.md new file mode 100644 index 00000000..e69de29b diff --git a/templates/registry/markdown/span.md b/templates/registry/markdown/span.md new file mode 100644 index 00000000..02b58a3f --- /dev/null +++ b/templates/registry/markdown/span.md @@ -0,0 +1,49 @@ +{%- set file_name = ctx.id | file_name -%} +{{- template.set_file_name("span/" ~ file_name ~ ".md") -}} + +## Group `{{ ctx.id }}` ({{ ctx.type }}) + +### Brief + +{{ ctx.brief | trim }} + +{{ ctx.note | trim }} + +Prefix: {{ ctx.prefix }} +Kind: {{ ctx.span_kind }} + +### Attributes + +{% for attribute in ctx.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} \ No newline at end of file diff --git a/templates/registry/markdown/spans.md b/templates/registry/markdown/spans.md new file mode 100644 index 00000000..28083c2a --- /dev/null +++ b/templates/registry/markdown/spans.md @@ -0,0 +1,50 @@ +# Semantic Convention Span Groups + +{% for group in ctx %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +{{ group.note | trim }} + +Prefix: {{ group.prefix }} +Kind: {{ group.span_kind }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} + {% endfor %} \ No newline at end of file diff --git a/templates/registry/markdown/weaver.yaml b/templates/registry/markdown/weaver.yaml index 75ab8b19..356ed8e6 100644 --- a/templates/registry/markdown/weaver.yaml +++ b/templates/registry/markdown/weaver.yaml @@ -2,9 +2,39 @@ templates: - pattern: registry.md filter: . application_mode: single + - pattern: attribute_group.md + filter: .groups[] | select(.type == "attribute_group") + application_mode: each + - pattern: attribute_groups.md + filter: .groups[] | select(.type == "attribute_group") + application_mode: single + - pattern: event.md + filter: .groups[] | select(.type == "event") + application_mode: each + - pattern: events.md + filter: .groups[] | select(.type == "event") + application_mode: single - pattern: group.md filter: .groups application_mode: each - pattern: groups.md filter: .groups - application_mode: single \ No newline at end of file + application_mode: single + - pattern: metric.md + filter: .groups[] | select(.type == "metric") + application_mode: each + - pattern: metrics.md + filter: .groups[] | select(.type == "metric") + application_mode: single + - pattern: resource.md + filter: .groups[] | select(.type == "resource") + application_mode: each + - pattern: resources.md + filter: .groups[] | select(.type == "resource") + application_mode: single + - pattern: span.md + filter: .groups[] | select(.type == "span") + application_mode: each + - pattern: spans.md + filter: .groups[] | select(.type == "span") + application_mode: single From 76fc30bb1e11cfca69c3d8cd45e3f3d95c7aef57 Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Sat, 9 Mar 2024 22:45:47 -0800 Subject: [PATCH 3/8] feature(cli): add quiet mode --- crates/weaver_logger/src/lib.rs | 2 + crates/weaver_logger/src/quiet.rs | 94 +++++++++++++++++++++++++++++++ src/cli.rs | 4 ++ src/main.rs | 17 +++++- 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 crates/weaver_logger/src/quiet.rs diff --git a/crates/weaver_logger/src/lib.rs b/crates/weaver_logger/src/lib.rs index 7c5edfcb..2a489173 100644 --- a/crates/weaver_logger/src/lib.rs +++ b/crates/weaver_logger/src/lib.rs @@ -6,6 +6,8 @@ #![deny(clippy::print_stdout)] #![deny(clippy::print_stderr)] +pub mod quiet; + use std::sync::atomic::AtomicUsize; use std::sync::{Arc, Mutex}; diff --git a/crates/weaver_logger/src/quiet.rs b/crates/weaver_logger/src/quiet.rs new file mode 100644 index 00000000..a990d571 --- /dev/null +++ b/crates/weaver_logger/src/quiet.rs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Logger in quiet mode. +//! This logger only logs errors and warnings. + +use std::sync::{Arc, Mutex}; +use crate::Logger; + +/// A quient logger that can be used to log messages to the console. +/// This logger is thread-safe and can be cloned. +/// Only logs errors and warnings are logged to the console. +#[derive(Default, Clone)] +pub struct QuietLogger { + logger: Arc>>, +} + +impl QuietLogger { + /// Creates a new logger. + pub fn new() -> Self { + Self { + logger: Arc::new(Mutex::new(paris::Logger::new())), + } + } +} + +impl Logger for QuietLogger { + /// Logs an trace message (only with debug enabled). + fn trace(&self, message: &str) -> &Self { + self + } + + /// Logs an info message. + fn info(&self, message: &str) -> &Self { + self + } + + /// Logs a warning message. + fn warn(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .warn(message); + self + } + + /// Logs an error message. + fn error(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .error(message); + self + } + + /// Logs a success message. + fn success(&self, message: &str) -> &Self { + self + } + + /// Logs a newline. + fn newline(&self, count: usize) -> &Self { + self + } + + /// Indents the logger. + fn indent(&self, count: usize) -> &Self { + self + } + + /// Stops a loading message. + fn done(&self) { + self.logger.lock().expect("Failed to lock logger").done(); + } + + /// Adds a style to the logger. + fn add_style(&self, name: &str, styles: Vec<&'static str>) -> &Self { + self + } + + /// Logs a loading message with a spinner. + fn loading(&self, message: &str) -> &Self { + self + } + + /// Forces the logger to not print a newline for the next message. + fn same(&self) -> &Self { + self + } + + /// Logs a message without icon. + fn log(&self, message: &str) -> &Self { + self + } +} \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 40d8cd79..61781de9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,6 +21,10 @@ pub struct Cli { #[arg(short, long, action = clap::ArgAction::Count)] pub debug: u8, + /// Turn the quiet mode on (i.e., minimal output) + #[arg(short, long)] + pub quiet: bool, + /// List of supported commands #[command(subcommand)] pub command: Option, diff --git a/src/main.rs b/src/main.rs index cae45e63..9b0bc2c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use clap::Parser; use registry::semconv_registry; -use weaver_logger::ConsoleLogger; +use weaver_logger::{ConsoleLogger, Logger}; +use weaver_logger::quiet::QuietLogger; use crate::cli::{Cli, Commands}; #[cfg(feature = "experimental")] @@ -22,8 +23,20 @@ mod search; fn main() { let cli = Cli::parse(); - let log = ConsoleLogger::new(cli.debug); + let start = std::time::Instant::now(); + if cli.quiet { + let log = QuietLogger::new(); + run_command(&cli, log); + } else { + let log = ConsoleLogger::new(cli.debug); + run_command(&cli, log); + }; + let elapsed = start.elapsed(); + println!("Total execution time: {:?}s", elapsed.as_secs_f64()); +} + +fn run_command(cli: &Cli, log: impl Logger + Sync + Clone) { match &cli.command { #[cfg(feature = "experimental")] Some(Commands::Resolve(params)) => { From 6f18c1597265236c0d96635ebcae579a406c4fff Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Mon, 11 Mar 2024 17:19:12 -0700 Subject: [PATCH 4/8] feature(template): reimplement template generation based on minijinja + jaq (jq-like filters) --- Cargo.lock | 25 +- Cargo.toml | 8 +- crates/weaver_forge/Cargo.toml | 4 +- .../weaver_forge/allowed-external-types.toml | 2 + .../expected_output/metric_groups.md | 2 - crates/weaver_forge/src/config.rs | 124 +++- crates/weaver_forge/src/debug.rs | 85 +++ crates/weaver_forge/src/error.rs | 151 ++++ crates/weaver_forge/src/filter.rs | 46 +- crates/weaver_forge/src/lib.rs | 661 +++++------------- crates/weaver_forge/src/registry.rs | 2 +- .../templates/test/attribute_groups.md | 2 +- crates/weaver_forge/templates/test/event.md | 12 +- crates/weaver_forge/templates/test/events.md | 2 +- crates/weaver_forge/templates/test/group.md | 16 +- crates/weaver_forge/templates/test/groups.md | 2 +- crates/weaver_forge/templates/test/metric.md | 20 +- .../templates/test/metric_group.md | 0 .../templates/test/metric_groups.md | 47 -- crates/weaver_forge/templates/test/metrics.md | 2 +- .../weaver_forge/templates/test/registry.md | 14 +- .../weaver_forge/templates/test/resource.md | 10 +- .../weaver_forge/templates/test/resources.md | 2 +- crates/weaver_forge/templates/test/scope.md | 0 crates/weaver_forge/templates/test/span.md | 14 +- crates/weaver_forge/templates/test/spans.md | 2 +- crates/weaver_logger/src/quiet.rs | 22 +- crates/weaver_resolved_schema/Cargo.toml | 2 +- docs/template-engine.md | 68 +- src/main.rs | 2 +- src/registry/generate.rs | 27 +- 31 files changed, 686 insertions(+), 690 deletions(-) delete mode 100644 crates/weaver_forge/expected_output/metric_groups.md create mode 100644 crates/weaver_forge/src/debug.rs create mode 100644 crates/weaver_forge/src/error.rs delete mode 100644 crates/weaver_forge/templates/test/metric_group.md delete mode 100644 crates/weaver_forge/templates/test/metric_groups.md delete mode 100644 crates/weaver_forge/templates/test/scope.md diff --git a/Cargo.lock b/Cargo.lock index 03af8280..e05c4cb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2695,9 +2695,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" dependencies = [ "base64", "bytes", @@ -3120,18 +3120,18 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck", "proc-macro2", @@ -3176,20 +3176,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3478,7 +3478,6 @@ dependencies = [ "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "windows-sys 0.48.0", @@ -3945,8 +3944,8 @@ name = "weaver_forge" version = "0.1.0" dependencies = [ "convert_case", - "glob", "globset", + "indexmap", "jaq-core", "jaq-interpret", "jaq-parse", diff --git a/Cargo.toml b/Cargo.toml index e520a6bd..c79e12b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,13 +25,13 @@ license = "Apache-2.0" publish = false [workspace.dependencies] -serde = { version = "1.0.196", features = ["derive"] } +serde = { version = "1.0.197", features = ["derive"] } serde_yaml = "0.9.32" -serde_json = "1.0.113" -thiserror = "1.0.56" +serde_json = "1.0.114" +thiserror = "1.0.57" ureq = "2.9.6" regex = "1.10.3" -rayon = "1.8.1" +rayon = "1.9.0" ordered-float = { version = "4.2.0", features = ["serde"] } walkdir = "2.5.0" diff --git a/crates/weaver_forge/Cargo.toml b/crates/weaver_forge/Cargo.toml index 016f93a9..d46b6ffc 100644 --- a/crates/weaver_forge/Cargo.toml +++ b/crates/weaver_forge/Cargo.toml @@ -13,14 +13,14 @@ weaver_resolver = { path = "../weaver_resolver" } weaver_resolved_schema = { path = "../weaver_resolved_schema" } weaver_semconv = { path = "../weaver_semconv" } -minijinja = { version = "1.0.12", features = ["loader", "custom_syntax"] } +minijinja = { version = "1.0.12", features = ["loader", "custom_syntax", "debug"] } convert_case = "0.6.0" -glob = "0.3.1" # ToDo: Should we still use this as globset is used anyway? globset = { version = "0.4.14", features = ["serde1"] } jaq-core = "1.2.1" jaq-std = "1.2.1" jaq-interpret = "1.2.1" jaq-parse = "1.0.2" +indexmap = "2.2.5" thiserror.workspace = true serde.workspace = true diff --git a/crates/weaver_forge/allowed-external-types.toml b/crates/weaver_forge/allowed-external-types.toml index 658b7951..ba4fbdb3 100644 --- a/crates/weaver_forge/allowed-external-types.toml +++ b/crates/weaver_forge/allowed-external-types.toml @@ -5,6 +5,8 @@ allowed_external_types = [ "serde::ser::Serialize", "serde::de::Deserialize", + "serde_json::value::Value", "weaver_logger::*", "weaver_resolved_schema::*", + "weaver_semconv::*", ] \ No newline at end of file diff --git a/crates/weaver_forge/expected_output/metric_groups.md b/crates/weaver_forge/expected_output/metric_groups.md deleted file mode 100644 index 28eae3a2..00000000 --- a/crates/weaver_forge/expected_output/metric_groups.md +++ /dev/null @@ -1,2 +0,0 @@ -# Semantic Convention Metric Group Groups - diff --git a/crates/weaver_forge/src/config.rs b/crates/weaver_forge/src/config.rs index b08161a9..07c36453 100644 --- a/crates/weaver_forge/src/config.rs +++ b/crates/weaver_forge/src/config.rs @@ -9,9 +9,10 @@ use convert_case::{Case, Casing}; use globset::{Glob, GlobSet, GlobSetBuilder}; use serde::Deserialize; -use crate::Error; -use crate::Error::InvalidConfigFile; +use crate::error::Error; +use crate::error::Error::InvalidConfigFile; use crate::filter::Filter; +use crate::WEAVER_YAML; /// Case convention for naming of functions and structs. #[derive(Deserialize, Clone, Debug)] @@ -61,10 +62,102 @@ pub struct TargetConfig { pub template_syntax: TemplateSyntax, /// Configuration for the templates. - #[serde(default)] + #[serde(default = "default_templates")] pub templates: Vec, } +fn default_templates() -> Vec { + vec![ + TemplateConfig { + pattern: Glob::new("**/registry.md").unwrap(), + filter: Filter::try_new(".").expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/attribute_group.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"attribute_group\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/attribute_groups.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"attribute_group\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/event.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"event\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/events.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"event\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/group.md").unwrap(), + filter: Filter::try_new(".groups").expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/groups.md").unwrap(), + filter: Filter::try_new(".groups").expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/metric.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"metric\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/metrics.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"metric\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/resource.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"resource\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/resources.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"resource\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/scope.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"scope\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/scopes.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"scope\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + TemplateConfig { + pattern: Glob::new("**/span.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"span\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Each, + }, + TemplateConfig { + pattern: Glob::new("**/spans.md").unwrap(), + filter: Filter::try_new(".groups[] | select(.type == \"span\")") + .expect("Invalid filter"), + application_mode: ApplicationMode::Single, + }, + ] +} + /// Application mode defining how to apply a template on the result of a /// filter applied on a registry. #[derive(Deserialize, Debug)] @@ -99,9 +192,13 @@ pub struct TemplateMatcher<'a> { glob_set: GlobSet, } -impl <'a> TemplateMatcher<'a> { +impl<'a> TemplateMatcher<'a> { pub fn matches>(&self, path: P) -> Vec<&'a TemplateConfig> { - self.glob_set.matches(path).into_iter().map(|i| &self.templates[i]).collect() + self.glob_set + .matches(path) + .into_iter() + .map(|i| &self.templates[i]) + .collect() } } @@ -209,7 +306,7 @@ impl CaseConvention { impl TargetConfig { pub fn try_new(lang_path: &Path) -> Result { - let config_file = lang_path.join("weaver.yaml"); + let config_file = lang_path.join(WEAVER_YAML); if config_file.exists() { let reader = std::fs::File::open(config_file.clone()).map_err(|e| InvalidConfigFile { @@ -233,11 +330,14 @@ impl TargetConfig { _ = builder.add(template.pattern.clone()); }); - builder.build().map_err(|e| Error::InvalidTemplatePattern { - error: e.to_string(), - }).map(|glob_set| TemplateMatcher { - templates: &self.templates, - glob_set, - }) + builder + .build() + .map_err(|e| Error::InvalidTemplatePattern { + error: e.to_string(), + }) + .map(|glob_set| TemplateMatcher { + templates: &self.templates, + glob_set, + }) } } diff --git a/crates/weaver_forge/src/debug.rs b/crates/weaver_forge/src/debug.rs new file mode 100644 index 00000000..88ea731f --- /dev/null +++ b/crates/weaver_forge/src/debug.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to help with debugging. + +use crate::error::Error; +use crate::error::Error::{CompoundError, TemplateEvaluationFailed}; +use indexmap::IndexMap; +use weaver_logger::Logger; + +/// Return a nice summary of the error. +pub(crate) fn error_summary(error: minijinja::Error) -> String { + format!("{:#}", error) +} + +/// Print deduplicated errors. +/// +/// This function prints the error message and the number of occurrences of +/// each error. If an error occurs only once, the error message is printed +/// as is. If an error occurs more than once, the error message is printed +/// once and the number of occurrences is printed as "and n more similar +/// errors". +/// +/// The order of the errors is preserved. +/// +/// # Arguments +/// +/// * `logger` - The logger to use for logging. +/// * `error` - The error to print. +pub fn print_dedup_errors(logger: impl Logger + Sync + Clone, error: Error) { + struct DedupError { + pub error: String, + pub occurrences: usize, + } + + let mut dedup_errs = IndexMap::new(); + match error { + CompoundError(errs) => { + for err in errs { + match err.clone() { + TemplateEvaluationFailed { + error_id, error, .. + } => { + _ = dedup_errs + .entry(error_id) + .and_modify(|e: &mut DedupError| e.occurrences += 1) + .or_insert(DedupError { + error, + occurrences: 1, + }); + } + _ => { + _ = dedup_errs + .entry(err.to_string()) + .and_modify(|e: &mut DedupError| e.occurrences += 1) + .or_insert(DedupError { + error: err.to_string(), + occurrences: 1, + }); + } + } + } + } + _ => { + _ = dedup_errs + .entry(error.to_string()) + .and_modify(|e| e.occurrences += 1) + .or_insert(DedupError { + error: error.to_string(), + occurrences: 1, + }); + } + } + dedup_errs.iter().for_each(|(_, err)| { + let output = match err.occurrences { + 1 => err.error.to_string(), + 2 => format!("{}\n\nFound 1 similar error", err.error), + _ => format!( + "{}\n\nFound {} similar errors", + err.error, + err.occurrences - 1 + ), + }; + _ = logger.error(&output); + }); +} diff --git a/crates/weaver_forge/src/error.rs b/crates/weaver_forge/src/error.rs new file mode 100644 index 00000000..2a65b918 --- /dev/null +++ b/crates/weaver_forge/src/error.rs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Error types and utilities. + +use crate::error::Error::CompoundError; +use std::path::PathBuf; +use weaver_resolved_schema::attribute::AttributeRef; + +/// Errors emitted by this crate. +#[derive(thiserror::Error, Debug, Clone)] +pub enum Error { + /// Invalid config file. + #[error("Invalid config file `{config_file}`: {error}")] + InvalidConfigFile { + /// Config file. + config_file: PathBuf, + /// Error message. + error: String, + }, + + /// Target not found. + #[error( + "Target `{target}` not found in `{root_path}`. Use the command `targets` to list supported targets." + )] + TargetNotSupported { + /// Root path. + root_path: String, + /// Target name. + target: String, + }, + + /// Invalid template directory. + #[error("Invalid template directory {template_dir}: {error}")] + InvalidTemplateDir { + /// Template directory. + template_dir: PathBuf, + /// Error message. + error: String, + }, + + /// Invalid telemetry schema. + #[error("Invalid telemetry schema {schema}: {error}")] + InvalidTelemetrySchema { + /// Schema file. + schema: PathBuf, + /// Error message. + error: String, + }, + + /// Invalid template file. + #[error("Invalid template file '{template}': {error}")] + InvalidTemplateFile { + /// Template path. + template: PathBuf, + /// Error message. + error: String, + }, + + /// Template evaluation failed. + #[error("Template evaluation error -> {error}")] + TemplateEvaluationFailed { + /// Template path. + template: PathBuf, + /// Error id used to deduplicate the error. + error_id: String, + /// Error message. + error: String, + }, + + /// Invalid template directory. + #[error("Invalid template directory: {0}")] + InvalidTemplateDirectory(PathBuf), + + /// Template file name undefined. + #[error("File name undefined in the template `{template}`. To resolve this, use the function `config(file_name = )` to set the file name.")] + TemplateFileNameUndefined { + /// Template path. + template: PathBuf, + }, + + /// Write generated code failed. + #[error("Writing of the generated code {template} failed: {error}")] + WriteGeneratedCodeFailed { + /// Template path. + template: PathBuf, + /// Error message. + error: String, + }, + + /// Attribute reference not found in the catalog. + #[error("Attribute reference {attr_ref} (group: {group_id}) not found in the catalog")] + AttributeNotFound { + /// Group id. + group_id: String, + /// Attribute reference. + attr_ref: AttributeRef, + }, + + /// Filter error. + #[error("Filter '{filter}' failed: {error}")] + FilterError { + /// Filter that caused the error. + filter: String, + /// Error message. + error: String, + }, + + /// Invalid template pattern. + #[error("Invalid template pattern: {error}")] + InvalidTemplatePattern { + /// Error message. + error: String, + }, + + /// The serialization of the context failed. + #[error("The serialization of the context failed: {error}")] + ContextSerializationFailed { + /// Error message. + error: String, + }, + + /// A generic container for multiple errors. + #[error("Errors:\n{0:#?}")] + CompoundError(Vec), +} + +/// Handles a list of errors and returns a compound error if the list is not +/// empty or () if the list is empty. +pub fn handle_errors(errors: Vec) -> Result<(), Error> { + if errors.is_empty() { + Ok(()) + } else { + Err(Error::compound_error(errors)) + } +} + +impl Error { + /// Creates a compound error from a list of errors. + /// Note: All compound errors are flattened. + pub fn compound_error(errors: Vec) -> Self { + CompoundError( + errors + .into_iter() + .flat_map(|e| match e { + CompoundError(errors) => errors, + e => vec![e], + }) + .collect(), + ) + } +} diff --git a/crates/weaver_forge/src/filter.rs b/crates/weaver_forge/src/filter.rs index 261426df..ff24b5fe 100644 --- a/crates/weaver_forge/src/filter.rs +++ b/crates/weaver_forge/src/filter.rs @@ -2,11 +2,11 @@ //! Filter JSON values using a simple expression language. +use crate::error::Error; use core::fmt; -use std::fmt::Debug; use jaq_interpret::{Ctx, FilterT, RcIter, Val}; use serde::de; -use crate::Error; +use std::fmt::Debug; /// A filter that can be applied to a JSON value. pub struct Filter { @@ -27,15 +27,19 @@ impl Filter { // If there are any errors, return them if !errs.is_empty() { - return Err(Error::CompoundError(errs.into_iter().map(|e| Error::FilterError { - filter: filter_expr.to_string(), - error: e.to_string() - }).collect())); + return Err(Error::CompoundError( + errs.into_iter() + .map(|e| Error::FilterError { + filter: filter_expr.to_string(), + error: e.to_string(), + }) + .collect(), + )); } let parsed_expr = parsed_expr.ok_or_else(|| Error::FilterError { filter: filter_expr.to_string(), - error: "No parsed expression".to_string() + error: "No parsed expression".to_string(), })?; Ok(Self { @@ -45,13 +49,13 @@ impl Filter { } /// Apply the filter to a JSON value and return the result as a JSON value. - pub fn apply(&self, json: serde_json::Value) -> Result { + pub fn apply(&self, ctx: serde_json::Value) -> Result { let inputs = RcIter::new(core::iter::empty()); - let mut filter_result = self.filter.run((Ctx::new([], &inputs), Val::from(json))); + let filter_result = self.filter.run((Ctx::new([], &inputs), Val::from(ctx))); let mut errs = Vec::new(); let mut values = Vec::new(); - while let Some(r) = filter_result.next() { + for r in filter_result { match r { Ok(v) => values.push(serde_json::Value::from(v)), Err(e) => errs.push(e), @@ -82,8 +86,8 @@ impl<'de> de::Visitor<'de> for FilterVisitor { } fn visit_str(self, value: &str) -> Result - where - E: de::Error, + where + E: de::Error, { Filter::try_new(value).map_err(E::custom) } @@ -91,23 +95,9 @@ impl<'de> de::Visitor<'de> for FilterVisitor { impl<'de> de::Deserialize<'de> for Filter { fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { deserializer.deserialize_str(FilterVisitor) } } - -#[cfg(test)] -mod tests { - use serde_json::json; - use crate::filter::Filter; - - #[test] - fn test_jaq() -> Result<(), crate::Error> { - let filter = Filter::try_new(".b").unwrap(); - let json = json!({"a": 1, "b": {"c": 1, "d": 2}}); - assert_eq!(filter.apply(json)?, json!([{"c": 1, "d": 2}])); - Ok(()) - } -} \ No newline at end of file diff --git a/crates/weaver_forge/src/lib.rs b/crates/weaver_forge/src/lib.rs index 5311ff39..7f5a8228 100644 --- a/crates/weaver_forge/src/lib.rs +++ b/crates/weaver_forge/src/lib.rs @@ -5,13 +5,13 @@ //! schemas. #![deny( -missing_docs, -clippy::print_stdout, -unstable_features, -unused_import_braces, -unused_qualifications, -unused_results, -unused_extern_crates + missing_docs, + clippy::print_stdout, + unstable_features, + unused_import_braces, + unused_qualifications, + unused_results, + unused_extern_crates )] use std::fmt::{Debug, Display, Formatter}; @@ -19,143 +19,35 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use glob::{glob, Paths}; -use minijinja::{Environment, path_loader, State, Value}; use minijinja::value::{from_args, Object}; +use minijinja::{path_loader, Environment, State, Value}; use rayon::iter::IntoParallelIterator; use rayon::iter::ParallelIterator; use serde::Serialize; use walkdir::{DirEntry, WalkDir}; +use error::Error; +use error::Error::{ + ContextSerializationFailed, InvalidTemplateDir, InvalidTemplateFile, TargetNotSupported, + TemplateEvaluationFailed, WriteGeneratedCodeFailed, +}; use weaver_logger::Logger; -use weaver_resolved_schema::attribute::AttributeRef; -use weaver_resolved_schema::catalog::Catalog; -use weaver_resolved_schema::registry::Registry; -use weaver_semconv::group::GroupType; use crate::config::{ApplicationMode, TargetConfig}; -use crate::Error::{CompoundError, InternalError, InvalidTemplateDir, InvalidTemplateDirectory, InvalidTemplateFile, TargetNotSupported, WriteGeneratedCodeFailed}; +use crate::debug::error_summary; +use crate::error::Error::InvalidConfigFile; use crate::extensions::case_converter::case_converter; use crate::registry::{TemplateGroup, TemplateRegistry}; mod config; +pub mod debug; +pub mod error; mod extensions; -pub mod registry; mod filter; +pub mod registry; -/// Errors emitted by this crate. -#[derive(thiserror::Error, Debug)] -pub enum Error { - /// Invalid config file. - #[error("Invalid config file `{config_file}`: {error}")] - InvalidConfigFile { - /// Config file. - config_file: PathBuf, - /// Error message. - error: String, - }, - - /// Target not found. - #[error( - "Target `{target}` not found in `{root_path}`. Use the command `targets` to list supported targets." - )] - TargetNotSupported { - /// Root path. - root_path: String, - /// Target name. - target: String, - }, - - /// Invalid template directory. - #[error("Invalid template directory {template_dir}: {error}")] - InvalidTemplateDir { - /// Template directory. - template_dir: PathBuf, - /// Error message. - error: String, - }, - - /// Invalid telemetry schema. - #[error("Invalid telemetry schema {schema}: {error}")] - InvalidTelemetrySchema { - /// Schema file. - schema: PathBuf, - /// Error message. - error: String, - }, - - /// Invalid template file. - #[error("Invalid template file '{template}': {error}")] - InvalidTemplateFile { - /// Template path. - template: PathBuf, - /// Error message. - error: String, - }, - - /// Invalid template directory. - #[error("Invalid template directory: {0}")] - InvalidTemplateDirectory(PathBuf), - - /// Internal error. - #[error("Internal error: {0}")] - InternalError(String), - - /// Template file name undefined. - #[error("File name undefined in the template `{template}`. To resolve this, use the function `config(file_name = )` to set the file name.")] - TemplateFileNameUndefined { - /// Template path. - template: PathBuf, - }, - - /// Write generated code failed. - #[error("Writing of the generated code {template} failed: {error}")] - WriteGeneratedCodeFailed { - /// Template path. - template: PathBuf, - /// Error message. - error: String, - }, - - /// Attribute reference not found in the catalog. - #[error("Attribute reference {attr_ref} (group: {group_id}) not found in the catalog")] - AttributeNotFound { - /// Group id. - group_id: String, - /// Attribute reference. - attr_ref: AttributeRef, - }, - - /// Filter error. - #[error("Filter '{filter}' failed: {error}")] - FilterError { - /// Filter that caused the error. - filter: String, - /// Error message. - error: String, - }, - - /// Invalid template pattern. - #[error("Invalid template pattern: {error}")] - InvalidTemplatePattern { - /// Error message. - error: String, - }, - - /// A generic container for multiple errors. - #[error("Errors:\n{0:#?}")] - CompoundError(Vec), -} - -/// Handles a list of errors and returns a compound error if the list is not -/// empty or () if the list is empty. -pub fn handle_errors(errors: Vec) -> Result<(), Error> { - if errors.is_empty() { - Ok(()) - } else { - Err(CompoundError(errors)) - } -} +/// Name of the Weaver configuration file. +pub const WEAVER_YAML: &str = "weaver.yaml"; /// General configuration for the generator. pub struct GeneratorConfig { @@ -172,23 +64,6 @@ impl Default for GeneratorConfig { } } -/// A pair {template, object} to generate code for. -#[derive(Debug)] -enum TemplateObjectPair<'a> { - Group { - template_path: PathBuf, - group: &'a TemplateGroup, - }, - Groups { - template_path: PathBuf, - groups: Vec<&'a TemplateGroup>, - }, - Registry { - template_path: PathBuf, - registry: &'a TemplateRegistry, - }, -} - /// A template object accessible from the template. #[derive(Debug, Clone)] struct TemplateObject { @@ -219,7 +94,7 @@ impl Object for TemplateObject { args: &[Value], ) -> Result { if name == "set_file_name" { - let (file_name, ): (&str, ) = from_args(args)?; + let (file_name,): (&str,) = from_args(args)?; *self.file_name.lock().unwrap() = file_name.to_string(); Ok(Value::from("")) } else { @@ -259,6 +134,17 @@ pub struct NewContext<'a> { pub ctx: &'a serde_json::Value, } +/// Convert a context into a serde_json::Value. +impl TryInto for NewContext<'_> { + type Error = Error; + + fn try_into(self) -> Result { + serde_json::to_value(self).map_err(|e| ContextSerializationFailed { + error: e.to_string(), + }) + } +} + impl TemplateEngine { /// Create a new template engine for the given target or return an error if /// the target does not exist or is not a directory. @@ -280,8 +166,6 @@ impl TemplateEngine { }) } - // ToDo Refactor InternalError - /// Generate artifacts from a serializable context and a template directory, /// in parallel. /// @@ -302,129 +186,111 @@ impl TemplateEngine { output_dir: &Path, ) -> Result<(), Error> { // List all files in the target directory and its subdirectories - let files: Vec = WalkDir::new(self.path.clone()).into_iter().filter_map(|e| { - // Skip directories that the owner of the running process does not - // have permission to access - e.ok() - }).filter(|dir_entry| dir_entry.path().is_file()).collect(); + let files: Vec = WalkDir::new(self.path.clone()) + .into_iter() + .filter_map(|e| { + // Skip directories that the owner of the running process does not + // have permission to access + e.ok() + }) + .filter(|dir_entry| dir_entry.path().is_file()) + .collect(); let config = TargetConfig::try_new(&self.path)?; let tmpl_matcher = config.template_matcher()?; - let context = serde_json::to_value(context).map_err(|e| InternalError(e.to_string()))?; - - let errs = files.into_par_iter().filter_map(|file| { - let relative_path = match file.path().strip_prefix(&self.path) { - Ok(relative_path) => relative_path, - Err(e) => return Some(InvalidTemplateDir { - template_dir: self.path.clone(), - error: e.to_string(), - }), - }; - - for template in tmpl_matcher.matches(relative_path) { - let filtered_result = match template.filter.apply(context.clone()) { - Ok(result) => result, - Err(e) => return Some(e), + // Create a read-only context for the filter evaluations + let context = serde_json::to_value(context).map_err(|e| ContextSerializationFailed { + error: e.to_string(), + })?; + + // Process all files in parallel + // - Filter the files that match the template pattern + // - Apply the filter to the context + // - Evaluate the template with the filtered context based on the + // application mode. + // - If the application mode is single, the filtered context is + // evaluated as a single object. + // - If the application mode is each, the filtered context is + // evaluated as an array of objects and each object is evaluated + // independently and in parallel with the same template. + let errs = files + .into_par_iter() + .filter_map(|file| { + let relative_path = match file.path().strip_prefix(&self.path) { + Ok(relative_path) => relative_path, + Err(e) => { + return Some(InvalidTemplateDir { + template_dir: self.path.clone(), + error: e.to_string(), + }); + } }; - match template.application_mode { - // The filtered result is evaluated as a single object - ApplicationMode::Single => { - if let Err(e) = self.evaluate_template(log.clone(), serde_json::to_value(NewContext {ctx: &filtered_result }).unwrap(), relative_path, output_dir) { - return Some(e); + for template in tmpl_matcher.matches(relative_path) { + let filtered_result = match template.filter.apply(context.clone()) { + Ok(result) => result, + Err(e) => return Some(e), + }; + + match template.application_mode { + // The filtered result is evaluated as a single object + ApplicationMode::Single => { + if let Err(e) = self.evaluate_template( + log.clone(), + NewContext { + ctx: &filtered_result, + } + .try_into() + .ok()?, + relative_path, + output_dir, + ) { + return Some(e); + } } - } - // The filtered result is evaluated as an array of objects - // and each object is evaluated independently and in parallel - // with the same template. - ApplicationMode::Each => { - if let Some(values) = filtered_result.as_array() { - let errs = values.into_par_iter().filter_map(|result| { - if let Err(e) = self.evaluate_template(log.clone(), serde_json::to_value(NewContext {ctx: &result}).unwrap(), relative_path, output_dir) { - return Some(e); + // The filtered result is evaluated as an array of objects + // and each object is evaluated independently and in parallel + // with the same template. + ApplicationMode::Each => { + if let Some(values) = filtered_result.as_array() { + let errs = values + .into_par_iter() + .filter_map(|result| { + if let Err(e) = self.evaluate_template( + log.clone(), + NewContext { ctx: result }.try_into().ok()?, + relative_path, + output_dir, + ) { + return Some(e); + } + None + }) + .collect::>(); + if !errs.is_empty() { + return Some(Error::compound_error(errs)); + } + } else if let Err(e) = self.evaluate_template( + log.clone(), + NewContext { + ctx: &filtered_result, } - None - }).collect::>(); - if errs.len() > 0 { - return Some(CompoundError(errs)); + .try_into() + .ok()?, + relative_path, + output_dir, + ) { + return Some(e); } - } else if let Err(e) = self.evaluate_template(log.clone(), serde_json::to_value(NewContext {ctx: &filtered_result }).unwrap(), relative_path, output_dir) { - return Some(e); } } } - } - None - }).collect::>(); + None + }) + .collect::>(); - handle_errors(errs) - } - - /// Generate assets from a semantic convention registry. - pub fn generate_registry( - &self, - log: impl Logger + Clone + Sync, - registry: &Registry, - catalog: &Catalog, - output_dir: &Path, - ) -> Result<(), Error> { - // Process recursively all files in the template directory - let mut lang_path = self.path.to_str().unwrap_or_default().to_string(); - let paths = if lang_path.is_empty() { - glob("**/*").map_err(|e| InternalError(e.to_string()))? - } else { - lang_path.push_str("/**/*"); - glob(lang_path.as_str()).map_err(|e| InternalError(e.to_string()))? - }; - - let template_registry = TemplateRegistry::try_from_resolved_registry(registry, catalog) - .map_err(|e| InternalError(e.to_string()))?; - - // List all {template, object} pairs to run in parallel the template - // engine as all pairs are independent. - self.list_registry_templates(&template_registry, paths)? - .into_par_iter() - .try_for_each(|pair| match pair { - TemplateObjectPair::Group { - template_path: relative_template_path, - group, - } => { - let ctx: serde_json::Value = serde_json::to_value(Context { - registry: &template_registry, - group: Some(group), - groups: None, - }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path.as_path(), output_dir) - } - TemplateObjectPair::Groups { - template_path: relative_template_path, - groups, - } => { - let ctx: serde_json::Value = serde_json::to_value(Context { - registry: &template_registry, - group: None, - groups: Some(groups), - }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path.as_path(), output_dir) - } - TemplateObjectPair::Registry { - template_path: relative_template_path, - registry, - } => { - let ctx: serde_json::Value = serde_json::to_value(Context { - registry, - group: None, - groups: None, - }) - .map_err(|e| InternalError(e.to_string()))?; - self.evaluate_template(log.clone(), ctx, relative_template_path.as_path(), output_dir) - } - })?; - - Ok(()) + error::handle_errors(errs) } fn evaluate_template( @@ -448,11 +314,20 @@ impl TemplateEngine { engine.add_global("template", Value::from_object(template_object.clone())); _ = log.loading(&format!("Generating file {}", template_file)); - let output = engine + let template = engine .get_template(template_file) - .map_err(|e| InternalError(e.to_string()))? - .render(ctx) - .map_err(|e| InternalError(e.to_string()))?; + .map_err(|e| InvalidTemplateFile { + template: template_path.to_path_buf(), + error: e.to_string(), + })?; + + let output = template + .render(ctx.clone()) + .map_err(|e| TemplateEvaluationFailed { + template: template_path.to_path_buf(), + error_id: e.to_string(), + error: error_summary(e), + })?; let generated_file = Self::save_generated_code(output_dir, template_object.file_name(), output)?; _ = log.success(&format!("Generated file {:?}", generated_file)); @@ -464,7 +339,10 @@ impl TemplateEngine { let mut env = Environment::new(); env.set_loader(path_loader(&self.path)); env.set_syntax(self.target_config.template_syntax.clone().into()) - .map_err(|e| InternalError(e.to_string()))?; + .map_err(|e| InvalidConfigFile { + config_file: self.path.join(WEAVER_YAML), + error: e.to_string(), + })?; // Register case conversion filters based on the target configuration env.add_filter( @@ -511,239 +389,6 @@ impl TemplateEngine { Ok(env) } - /// Lists all {template, object} pairs derived from a template directory and a given - /// semantic convention registry. - fn list_registry_templates<'a>( - &self, - registry: &'a TemplateRegistry, - paths: Paths, - ) -> Result>, Error> { - let mut templates = Vec::new(); - - for entry in paths { - if let Ok(tmpl_file_path) = entry { - if tmpl_file_path.is_dir() { - continue; - } - let relative_path = - tmpl_file_path - .strip_prefix(&self.path) - .map_err(|e| InvalidTemplateDir { - template_dir: self.path.clone(), - error: e.to_string(), - })?; - let tmpl_file = tmpl_file_path.to_str().ok_or(InvalidTemplateFile { - template: tmpl_file_path.clone(), - error: "".to_string(), - })?; - - if tmpl_file.ends_with(".j2") { - // Files with .j2 are either macros or included files - // imported from the template files, so we skip them. - continue; - } - - if tmpl_file.ends_with("weaver.yaml") { - // Skip weaver configuration file. - continue; - } - - match tmpl_file_path.file_stem().and_then(|s| s.to_str()) { - Some("attribute_group") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::AttributeGroup)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("event") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Event)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("group") => { - registry.groups.iter().for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("metric") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Metric)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("metric_group") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::MetricGroup)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("registry") => { - templates.push(TemplateObjectPair::Registry { - template_path: relative_path.to_path_buf(), - registry, - }); - } - Some("resource") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Resource)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("scope") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Scope)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("span") => { - registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Span)) - .for_each(|group| { - templates.push(TemplateObjectPair::Group { - template_path: relative_path.to_path_buf(), - group, - }) - }); - } - Some("attribute_groups") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::AttributeGroup)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("events") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Event)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("groups") => { - let groups = registry.groups.iter().collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("metrics") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Metric)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("metric_groups") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::MetricGroup)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("resources") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Resource)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("scopes") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Scope)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - Some("spans") => { - let groups = registry - .groups - .iter() - .filter(|group| matches!(group.r#type, GroupType::Span)) - .collect::>(); - templates.push(TemplateObjectPair::Groups { - template_path: relative_path.to_path_buf(), - groups, - }) - } - _ => templates.push(TemplateObjectPair::Registry { - template_path: relative_path.to_path_buf(), - registry, - }), - } - } else { - return Err(InvalidTemplateDirectory(self.path.clone())); - } - } - - Ok(templates) - } - /// Save the generated code to the output directory. fn save_generated_code( output_dir: &Path, @@ -785,6 +430,8 @@ mod tests { use weaver_resolver::SchemaResolver; use weaver_semconv::SemConvRegistry; + use crate::registry::TemplateRegistry; + #[test] fn test() { let logger = TestLogger::default(); @@ -797,13 +444,17 @@ mod tests { SchemaResolver::resolve_semantic_convention_registry(&mut registry, logger.clone()) .expect("Failed to resolve registry"); + let template_registry = + TemplateRegistry::try_from_resolved_registry(&schema.registries[0], &schema.catalog) + .unwrap_or_else(|e| { + panic!( + "Failed to create the context for the template evaluation: {:?}", + e + ) + }); + engine - .generate_registry( - logger, - &schema.registries[0], - &schema.catalog, - Path::new("observed_output"), - ) + .generate(logger, &template_registry, Path::new("observed_output")) .expect("Failed to generate registry assets"); assert!(cmp_dir("expected_output", "observed_output").unwrap()); @@ -862,9 +513,9 @@ mod tests { .collect::>() .is_empty() || !observed_files - .difference(&expected_files) - .collect::>() - .is_empty() + .difference(&expected_files) + .collect::>() + .is_empty() { are_identical = false; } diff --git a/crates/weaver_forge/src/registry.rs b/crates/weaver_forge/src/registry.rs index 80b7f972..e57e7d77 100644 --- a/crates/weaver_forge/src/registry.rs +++ b/crates/weaver_forge/src/registry.rs @@ -4,7 +4,7 @@ //! catalog are resolved to the actual catalog entries to ease the template //! evaluation. -use crate::Error; +use crate::error::Error; use serde::{Deserialize, Serialize}; use weaver_resolved_schema::attribute::Attribute; use weaver_resolved_schema::catalog::Catalog; diff --git a/crates/weaver_forge/templates/test/attribute_groups.md b/crates/weaver_forge/templates/test/attribute_groups.md index ae59a378..5cd2168b 100644 --- a/crates/weaver_forge/templates/test/attribute_groups.md +++ b/crates/weaver_forge/templates/test/attribute_groups.md @@ -1,6 +1,6 @@ # Semantic Convention Attribute Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/event.md b/crates/weaver_forge/templates/test/event.md index 5ba06ad0..43736aa7 100644 --- a/crates/weaver_forge/templates/test/event.md +++ b/crates/weaver_forge/templates/test/event.md @@ -1,18 +1,18 @@ -{%- set file_name = ctx.group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("event/" ~ file_name ~ ".md") -}} -# Group `{{ group.id }}` ({{ group.type }}) +# Group `{{ ctx.id }}` ({{ ctx.type }}) ## Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -Prefix: {{ group.prefix }} -Name: {{ group.name }} +Prefix: {{ ctx.prefix }} +Name: {{ ctx.name }} ## Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} ### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/events.md b/crates/weaver_forge/templates/test/events.md index bcc09d59..eae78bc1 100644 --- a/crates/weaver_forge/templates/test/events.md +++ b/crates/weaver_forge/templates/test/events.md @@ -1,6 +1,6 @@ # Semantic Convention Event Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/group.md b/crates/weaver_forge/templates/test/group.md index c58c8e3c..802ccb77 100644 --- a/crates/weaver_forge/templates/test/group.md +++ b/crates/weaver_forge/templates/test/group.md @@ -1,17 +1,17 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("group/" ~ file_name ~ ".md") -}} -# Group `{{ group.id }}` ({{ group.type }}) +# Group `{{ ctx.id }}` ({{ ctx.type }}) ## Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -prefix: {{ group.prefix }} +prefix: {{ ctx.prefix }} ## Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} ### Attribute `{{ attribute.name }}` {{ attribute.brief }} @@ -47,8 +47,8 @@ prefix: {{ group.prefix }} ## Provenance -Source: {{ group.lineage.provenance }} +Source: {{ ctx.lineage.provenance }} -{% for item in group.lineage.attributes -%} -item: {{ group.lineage.attributes[item] }} +{% for item in ctx.lineage.attributes -%} +item: {{ ctx.lineage.attributes[item] }} {% endfor -%} \ No newline at end of file diff --git a/crates/weaver_forge/templates/test/groups.md b/crates/weaver_forge/templates/test/groups.md index 3e8188f9..8b072179 100644 --- a/crates/weaver_forge/templates/test/groups.md +++ b/crates/weaver_forge/templates/test/groups.md @@ -1,6 +1,6 @@ # Semantic Convention Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/metric.md b/crates/weaver_forge/templates/test/metric.md index fc58ca10..d45df277 100644 --- a/crates/weaver_forge/templates/test/metric.md +++ b/crates/weaver_forge/templates/test/metric.md @@ -1,23 +1,23 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("metric/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -{{ group.note | trim }} +{{ ctx.note | trim }} -Prefix: {{ group.prefix }} -Metric: {{ group.metric_name }} -Instrument: {{ group.instrument }} -Unit: {{ group.unit }} -Stability: {{ group.stability | capitalize }} +Prefix: {{ ctx.prefix }} +Metric: {{ ctx.metric_name }} +Instrument: {{ ctx.instrument }} +Unit: {{ ctx.unit }} +Stability: {{ ctx.stability | capitalize }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/metric_group.md b/crates/weaver_forge/templates/test/metric_group.md deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/weaver_forge/templates/test/metric_groups.md b/crates/weaver_forge/templates/test/metric_groups.md deleted file mode 100644 index 6e721a57..00000000 --- a/crates/weaver_forge/templates/test/metric_groups.md +++ /dev/null @@ -1,47 +0,0 @@ -# Semantic Convention Metric Group Groups - -{% for group in groups %} -## Group `{{ group.id }}` ({{ group.type }}) - -### Brief - -{{ group.brief | trim }} - -prefix: {{ group.prefix }} - -### Attributes - -{% for attribute in group.attributes %} -#### Attribute `{{ attribute.name }}` - -{{ attribute.brief }} - -{% if attribute.note %} -{{ attribute.note | trim }} -{% endif %} - -{%- if attribute.requirement_level == "required" %} -- Requirement Level: Required - {%- elif attribute.requirement_level.conditionally_required %} -- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} - {%- elif attribute.requirement_level == "recommended" %} -- Requirement Level: Recommended - {%- else %} -- Requirement Level: Optional - {%- endif %} - {% if attribute.tag %} -- Tag: {{ attribute.tag }} - {% endif %} - {%- include "attribute_type.j2" %} - {%- include "examples.j2" -%} - {%- if attribute.sampling_relevant %} -- Sampling relevant: {{ attribute.sampling_relevant }} - {%- endif %} - {%- if attribute.deprecated %} -- Deprecated: {{ attribute.deprecated }} - {%- endif %} - {% if attribute.stability %} -- Stability: {{ attribute.stability | capitalize }} - {% endif %} - {% endfor %} - {% endfor %} diff --git a/crates/weaver_forge/templates/test/metrics.md b/crates/weaver_forge/templates/test/metrics.md index df460b78..641805cb 100644 --- a/crates/weaver_forge/templates/test/metrics.md +++ b/crates/weaver_forge/templates/test/metrics.md @@ -1,6 +1,6 @@ # Semantic Convention Metric Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/registry.md b/crates/weaver_forge/templates/test/registry.md index e1c26c56..3bd4b2c5 100644 --- a/crates/weaver_forge/templates/test/registry.md +++ b/crates/weaver_forge/templates/test/registry.md @@ -3,49 +3,49 @@ Url: {{ registry_url }} # Attribute Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "attribute_group" %} - [{{ group.id }}](attribute_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Events -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "event" %} - [{{ group.id }}](event/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metrics -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric" %} - [{{ group.id }}](metric/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Metric Groups -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "metric_group" %} - [{{ group.id }}](metric_group/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Resource -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "resource" %} - [{{ group.id }}](resource/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Scope -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "scope" %} - [{{ group.id }}](scope/{{ group.id | file_name }}.md) {%- endif %} {%- endfor %} # Span -{% for group in registry.groups -%} +{% for group in ctx.groups -%} {%- if group.type == "span" %} - [{{ group.id }}](span/{{ group.id | file_name }}.md) {%- endif %} diff --git a/crates/weaver_forge/templates/test/resource.md b/crates/weaver_forge/templates/test/resource.md index 391d364b..5fb7efa6 100644 --- a/crates/weaver_forge/templates/test/resource.md +++ b/crates/weaver_forge/templates/test/resource.md @@ -1,17 +1,17 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("resource/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -prefix: {{ group.prefix }} +prefix: {{ ctx.prefix }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/resources.md b/crates/weaver_forge/templates/test/resources.md index 8a33dfbc..955b76fc 100644 --- a/crates/weaver_forge/templates/test/resources.md +++ b/crates/weaver_forge/templates/test/resources.md @@ -1,6 +1,6 @@ # Semantic Convention Resource Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_forge/templates/test/scope.md b/crates/weaver_forge/templates/test/scope.md deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/weaver_forge/templates/test/span.md b/crates/weaver_forge/templates/test/span.md index 432bc8e9..02b58a3f 100644 --- a/crates/weaver_forge/templates/test/span.md +++ b/crates/weaver_forge/templates/test/span.md @@ -1,20 +1,20 @@ -{%- set file_name = group.id | file_name -%} +{%- set file_name = ctx.id | file_name -%} {{- template.set_file_name("span/" ~ file_name ~ ".md") -}} -## Group `{{ group.id }}` ({{ group.type }}) +## Group `{{ ctx.id }}` ({{ ctx.type }}) ### Brief -{{ group.brief | trim }} +{{ ctx.brief | trim }} -{{ group.note | trim }} +{{ ctx.note | trim }} -Prefix: {{ group.prefix }} -Kind: {{ group.span_kind }} +Prefix: {{ ctx.prefix }} +Kind: {{ ctx.span_kind }} ### Attributes -{% for attribute in group.attributes %} +{% for attribute in ctx.attributes %} #### Attribute `{{ attribute.name }}` {{ attribute.brief }} diff --git a/crates/weaver_forge/templates/test/spans.md b/crates/weaver_forge/templates/test/spans.md index ebe2f969..28083c2a 100644 --- a/crates/weaver_forge/templates/test/spans.md +++ b/crates/weaver_forge/templates/test/spans.md @@ -1,6 +1,6 @@ # Semantic Convention Span Groups -{% for group in groups %} +{% for group in ctx %} ## Group `{{ group.id }}` ({{ group.type }}) ### Brief diff --git a/crates/weaver_logger/src/quiet.rs b/crates/weaver_logger/src/quiet.rs index a990d571..086468fb 100644 --- a/crates/weaver_logger/src/quiet.rs +++ b/crates/weaver_logger/src/quiet.rs @@ -3,8 +3,8 @@ //! Logger in quiet mode. //! This logger only logs errors and warnings. -use std::sync::{Arc, Mutex}; use crate::Logger; +use std::sync::{Arc, Mutex}; /// A quient logger that can be used to log messages to the console. /// This logger is thread-safe and can be cloned. @@ -24,13 +24,13 @@ impl QuietLogger { } impl Logger for QuietLogger { - /// Logs an trace message (only with debug enabled). - fn trace(&self, message: &str) -> &Self { + /// Logs a trace message (only with debug enabled). + fn trace(&self, _message: &str) -> &Self { self } /// Logs an info message. - fn info(&self, message: &str) -> &Self { + fn info(&self, _message: &str) -> &Self { self } @@ -53,17 +53,17 @@ impl Logger for QuietLogger { } /// Logs a success message. - fn success(&self, message: &str) -> &Self { + fn success(&self, _message: &str) -> &Self { self } /// Logs a newline. - fn newline(&self, count: usize) -> &Self { + fn newline(&self, _count: usize) -> &Self { self } /// Indents the logger. - fn indent(&self, count: usize) -> &Self { + fn indent(&self, _count: usize) -> &Self { self } @@ -73,12 +73,12 @@ impl Logger for QuietLogger { } /// Adds a style to the logger. - fn add_style(&self, name: &str, styles: Vec<&'static str>) -> &Self { + fn add_style(&self, _name: &str, _styles: Vec<&'static str>) -> &Self { self } /// Logs a loading message with a spinner. - fn loading(&self, message: &str) -> &Self { + fn loading(&self, _message: &str) -> &Self { self } @@ -88,7 +88,7 @@ impl Logger for QuietLogger { } /// Logs a message without icon. - fn log(&self, message: &str) -> &Self { + fn log(&self, _message: &str) -> &Self { self } -} \ No newline at end of file +} diff --git a/crates/weaver_resolved_schema/Cargo.toml b/crates/weaver_resolved_schema/Cargo.toml index 34b00b87..3b02528e 100644 --- a/crates/weaver_resolved_schema/Cargo.toml +++ b/crates/weaver_resolved_schema/Cargo.toml @@ -15,4 +15,4 @@ serde.workspace = true ordered-float.workspace = true [dev-dependencies] -serde_json = "1.0.113" \ No newline at end of file +serde_json = "1.0.114" \ No newline at end of file diff --git a/docs/template-engine.md b/docs/template-engine.md index 734ebdcc..72ecb904 100644 --- a/docs/template-engine.md +++ b/docs/template-engine.md @@ -66,10 +66,6 @@ produced from the template: {{- template.set_file_name("span/" ~ file_name ~ ".md") -}} ``` -> Note: Other naming conventions might be introduced in the future to facilitate -the generation of assets (e.g., groups_per_prefix.md could be used to call the -template with a list of groups sharing the same prefix). - ## Configuration File The configuration file `weaver.yaml` is optional. It allows configuring the @@ -107,6 +103,70 @@ template_syntax: variable_end: "}}" comment_start: "{#" comment_end: "#}" + +# Please uncomment the following templates to override the default template +# mapping. Each template mapping specifies a jaq filter (compatible with jq) +# to apply to every file matching the pattern. The application_mode specifies +# how the template should be applied. The application_mode can be `each` or +# `single`. The `each` mode will evaluate the template for each object selected +# by the jaq filter. The `single` mode will evaluate the template once with all +# the objects selected by the jq filter. +# +# Note: jaq is a Rust reimplementation of jq. Most of the jq filters are +# supported. For more information, see https://github.com/01mf02/jaq +# +# templates: +# - pattern: "**/registry.md" +# filter: "." +# application_mode: single +# - pattern: "**/attribute_group.md" +# filter: ".groups[] | select(.type == \"attribute_group\")" +# application_mode: each +# - pattern: "**/attribute_groups.md" +# filter: ".groups[] | select(.type == \"attribute_group\")" +# application_mode: single +# - pattern: "**/event.md" +# filter: ".groups[] | select(.type == \"event\")" +# application_mode: each +# - pattern: "**/events.md" +# filter: ".groups[] | select(.type == \"event\")" +# application_mode: single +# - pattern: "**/group.md" +# filter: ".groups[] | select(.type == \"group\")" +# application_mode: each +# - pattern: "**/groups.md" +# filter: ".groups[] | select(.type == \"group\")" +# application_mode: single +# - pattern: "**/metric.md" +# filter: ".groups[] | select(.type == \"metric\")" +# application_mode: each +# - pattern: "**/metrics.md" +# filter: ".groups[] | select(.type == \"metric\")" +# application_mode: single +# - pattern: "**/metric_group.md" +# filter: ".groups[] | select(.type == \"metric_group\")" +# application_mode: each +# - pattern: "**/metric_groups.md" +# filter: ".groups[] | select(.type == \"metric_group\")" +# application_mode: single +# - pattern: "**/resource.md" +# filter: ".groups[] | select(.type == \"resource\")" +# application_mode: each +# - pattern: "**/resources.md" +# filter: ".groups[] | select(.type == \"resource\")" +# application_mode: single +# - pattern: "**/scope.md" +# filter: ".groups[] | select(.type == \"scope\")" +# application_mode: each +# - pattern: "**/scopes.md" +# filter: ".groups[] | select(.type == \"scope\")" +# application_mode: single +# - pattern: "**/span.md" +# filter: ".groups[] | select(.type == \"span\")" +# application_mode: each +# - pattern: "**/spans.md" +# filter: ".groups[] | select(.type == \"span\")" +# application_mode: single ``` Supported case converters: diff --git a/src/main.rs b/src/main.rs index 9b0bc2c7..77dacb80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use clap::Parser; use registry::semconv_registry; -use weaver_logger::{ConsoleLogger, Logger}; use weaver_logger::quiet::QuietLogger; +use weaver_logger::{ConsoleLogger, Logger}; use crate::cli::{Cli, Commands}; #[cfg(feature = "experimental")] diff --git a/src/registry/generate.rs b/src/registry/generate.rs index f7ab99bc..15e94211 100644 --- a/src/registry/generate.rs +++ b/src/registry/generate.rs @@ -6,8 +6,9 @@ use clap::Args; use std::path::PathBuf; use weaver_cache::Cache; -use weaver_forge::{GeneratorConfig, TemplateEngine}; +use weaver_forge::debug::print_dedup_errors; use weaver_forge::registry::TemplateRegistry; +use weaver_forge::{GeneratorConfig, TemplateEngine}; use weaver_logger::Logger; use weaver_resolver::SchemaResolver; @@ -74,14 +75,20 @@ pub(crate) fn command( ) .expect("Failed to create template engine"); - let template_registry = TemplateRegistry::try_from_resolved_registry(&schema.registries[0], &schema.catalog) - .unwrap_or_else(|e| panic!("Failed to create the context for the template evaluation: {:?}", e)); - - engine.generate( - logger.clone(), - &template_registry, - args.output.as_path(), - ).expect("Failed to generate artifacts"); + let template_registry = + TemplateRegistry::try_from_resolved_registry(&schema.registries[0], &schema.catalog) + .unwrap_or_else(|e| { + panic!( + "Failed to create the context for the template evaluation: {:?}", + e + ) + }); - logger.success("Artifacts generated successfully"); + match engine.generate(logger.clone(), &template_registry, args.output.as_path()) { + Ok(_) => logger.success("Artifacts generated successfully"), + Err(e) => { + print_dedup_errors(logger.clone(), e); + std::process::exit(1); + } + }; } From 90e93664921d0a6c72dbb629a06c8044413fac15 Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Mon, 11 Mar 2024 21:59:07 -0700 Subject: [PATCH 5/8] chore(doc): update README.md to describe check and generate sub-commands --- README.md | 107 +++++++++++++------------------ crates/weaver_semconv/src/lib.rs | 2 +- 2 files changed, 44 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index adafd45d..354fd94a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Commands: Options: -d, --debug... Turn debugging information on + -q, --quiet Turn the quiet mode on (i.e., minimal output) -h, --help Print help -V, --version Print version ``` @@ -61,76 +62,54 @@ Manage Semantic Convention Registry Usage: weaver registry Commands: - check Validates a registry (i.e., parsing, resolution of references, extends clauses, and constraints) - generate Generates documentation or code for a registry (not yet implemented) - resolve Resolves a registry (not yet implemented) - search Searches a registry (not yet implemented) - stats Calculate and display a set of general statistics on a registry (not yet implemented) - help Print this message or the help of the given subcommand(s) + check Validates a registry (i.e., parsing, resolution of references, extends clauses, and constraints) + generate Generates artifacts from a registry + resolve Resolves a registry (not yet implemented) + search Searches a registry (not yet implemented) + stats Calculate and display a set of general statistics on a registry (not yet implemented) + update-markdown Update markdown files that contain markers indicating the templates used to update the specified sections + help Print this message or the help of the given subcommand(s) Options: -h, --help Print help ``` -### Command `search` (Experimental) - -This command provides an interactive terminal UI, allowing users to search for -attributes and metrics specified within a given semantic convention registry or -a telemetry schema (including dependencies). +### Sub-Command `registry check` -To search into the OpenTelemetry Semantic Convention Registry, run the following -command: - -```bash -weaver search registry https://github.com/open-telemetry/semantic-conventions.git model ``` +Validates a registry (i.e., parsing, resolution of references, extends clauses, and constraints) -To search into a telemetry schema, run the following command: - -```bash -weaver search schema demo/app-telemetry-schema.yaml -``` - -This search engine leverages [Tantivy](https://github.com/quickwit-oss/tantivy) -and supports a simple [search syntax](https://docs.rs/tantivy/latest/tantivy/query/struct.QueryParser.html) -in the search bar. - -### Command `resolve` (Experimental) +Usage: weaver registry check [OPTIONS] -This command resolves a schema or a semantic convention registry (not yet -implemented) and displays the result on the standard output. -Alternatively, the result can be written to a file if specified using the -`--output` option. This command is primarily used for validating and debugging -telemetry schemas and semantic convention registries. - -```bash -weaver resolve schema telemetry-schema.yaml --output telemetry-schema-resolved.yaml +Options: + -r, --registry + Local path or Git URL of the semantic convention registry to check [default: https://github.com/open-telemetry/semantic-conventions.git] + -d, --registry-git-sub-dir + Optional path in the Git repository where the semantic convention registry is located [default: model] + -h, --help + Print help ``` -A "resolved schema" is one where: -- All references have been resolved and expanded. -- All overrides have been applied. -- This resolved schema is what the code generator and upcoming plugins utilize. - -### Command `gen-client` (Experimental) +### Sub-Command `registry generate` -This command generates a client SDK from a telemetry schema for a given language -specified with the `--language` option. - -```bash -weaver gen-client --schema telemetry-schema.yaml --language go ``` +Generates artifacts from a registry -In the future, users will be able to specify the protocol to use for the generated -client SDK (i.e. OTLP or OTel Arrow Protocol) and few others options. - -### Command `languages` (Experimental) +Usage: weaver registry generate [OPTIONS] [OUTPUT] -This command displays all the languages for which a client SDK/API can -be generated. +Arguments: + Target to generate the artifacts for + [OUTPUT] Path to the directory where the generated artifacts will be saved. Default is the `output` directory [default: output] -```bash -weaver languages +Options: + -t, --templates + Path to the directory where the templates are located. Default is the `templates` directory [default: templates] + -r, --registry + Local path or Git URL of the semantic convention registry [default: https://github.com/open-telemetry/semantic-conventions.git] + -d, --registry-git-sub-dir + Optional path in the Git repository where the semantic convention registry is located [default: model] + -h, --help + Print help ``` ### Crates Layout @@ -153,16 +132,16 @@ crates.io. The following is a list of crates in the workspace, along with a brief description and the current status of each crate: -| Crate | Description | Status | -|-------------------------------------------------------------------|---------------------------------------------------------|------------------------| -| [weaver_semconv](crates/weaver_semconv/README.md) | Semantic Convention Registry Data Model | Alpha; Need more tests | -| [weaver_version](crates/weaver_version/README.md) | OpenTelemetry Schema Versioning Data Model | Alpha; Need more tests | -| [weaver_resolved_schema](crates/weaver_resolved_schema/README.md) | Resolved Schema Data Model | Work-In-Progress | -| [weaver_schema](crates/weaver_schema/README.md) | Telemetry Schema Data Model | Work-In-Progress | -| [weaver_resolver](crates/weaver_resolver/README.md) | Telemetry Schema Resolution Process | Work-In-Progress | -| [weaver_cache](crates/weaver_cache/README.md) | Telemetry Schema and Semantic Convention Registry Cache | Work-In-Progress | -| [weaver_logger](crates/weaver_logger/README.md) | Generic logger supported colorized output | Alpha | -| [weaver_template](crates/weaver_template/README.md) | Functions and Filters used in the template engine | Work-In-Progress | +| Crate | Description | Status | +|-------------------------------------------------------------------|----------------------------------------------------------------------|-------------------------| +| [weaver_semconv](crates/weaver_semconv/README.md) | Semantic Convention Registry Data Model | Alpha; Need more tests | +| [weaver_version](crates/weaver_version/README.md) | OpenTelemetry Schema Versioning Data Model | Alpha; Need more tests | +| [weaver_resolved_schema](crates/weaver_resolved_schema/README.md) | Resolved Schema Data Model | Work-In-Progress | +| [weaver_schema](crates/weaver_schema/README.md) | Telemetry Schema Data Model | Work-In-Progress | +| [weaver_resolver](crates/weaver_resolver/README.md) | Telemetry Schema Resolution Process | Work-In-Progress | +| [weaver_cache](crates/weaver_cache/README.md) | Telemetry Schema and Semantic Convention Registry Cache | Work-In-Progress | +| [weaver_logger](crates/weaver_logger/README.md) | Generic logger supported colorized output | Alpha | +| [weaver_forge](crates/weaver_forge/README.md) | Template engine used to generate artifacts from any serde json value | Alpha; Need more tests | Note 1: Alpha status means that the crate is in a usable state but may have limited functionality and/or may not be fully tested. diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs index 9cdb2f09..8d646e8b 100644 --- a/crates/weaver_semconv/src/lib.rs +++ b/crates/weaver_semconv/src/lib.rs @@ -732,7 +732,7 @@ impl SemConvRegistry { format!("{}.{}", prefix, id) }; if let AttributeSpec::Id { id, .. } = &mut attr { - *id = fq_attr_id.clone(); + id.clone_from(&fq_attr_id) } let prev_val = self.all_attributes.insert( fq_attr_id.clone(), From 1fdb67513e4d8e64f775a4713b57b9c3e9d026c4 Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Mon, 11 Mar 2024 22:22:51 -0700 Subject: [PATCH 6/8] chore(build): fix clippy issues --- Cargo.lock | 12 ++++++------ crates/weaver_resolver/src/metrics.rs | 2 +- crates/weaver_schema/src/attribute.rs | 24 ++++++++++++++---------- crates/weaver_schema/src/lib.rs | 2 +- justfile | 1 + 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e05c4cb5..6e0d0502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2485,9 +2485,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -3392,18 +3392,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", diff --git a/crates/weaver_resolver/src/metrics.rs b/crates/weaver_resolver/src/metrics.rs index 0a584555..8f8b8dfe 100644 --- a/crates/weaver_resolver/src/metrics.rs +++ b/crates/weaver_resolver/src/metrics.rs @@ -93,7 +93,7 @@ pub fn resolve_metrics( // Initialize all/required_shared_attributes only if first metric. if i == 0 { - all_shared_attributes = inherited_attrs.clone(); + all_shared_attributes.clone_from(&inherited_attrs); all_shared_attributes .iter() .filter(|attr| attr.is_required()) diff --git a/crates/weaver_schema/src/attribute.rs b/crates/weaver_schema/src/attribute.rs index 3d786685..96764119 100644 --- a/crates/weaver_schema/src/attribute.rs +++ b/crates/weaver_schema/src/attribute.rs @@ -266,27 +266,31 @@ impl Attribute { pub fn set_tags(&mut self, tags: &Option) { match self { Attribute::Ref { tags: tags_ref, .. } => { - *tags_ref = tags.clone(); + tags_ref.clone_from(tags); } Attribute::Id { tags: tags_id, .. } => { - *tags_id = tags.clone(); + tags_id.clone_from(tags); } Attribute::AttributeGroupRef { tags: tags_group, .. } => { - *tags_group = tags.clone(); + tags_group.clone_from(tags); } Attribute::ResourceRef { tags: tags_resource, .. } => { - *tags_resource = tags.clone(); + tags_resource.clone_from(tags); } - Attribute::SpanRef { tags, .. } => { - *tags = tags.clone(); + Attribute::SpanRef { + tags: span_tags, .. + } => { + span_tags.clone_from(tags); } - Attribute::EventRef { tags, .. } => { - *tags = tags.clone(); + Attribute::EventRef { + tags: event_tags, .. + } => { + event_tags.clone_from(tags); } } } @@ -339,7 +343,7 @@ impl Attribute { // Override process. // Use the field values from the reference when defined in the reference. if let Some(brief_from_ref) = brief_from_ref { - brief = brief_from_ref.clone(); + brief.clone_from(brief_from_ref); } if let Some(requirement_level_from_ref) = requirement_level_from_ref { requirement_level = requirement_level_from_ref.clone(); @@ -354,7 +358,7 @@ impl Attribute { sampling_relevant = Some(*sampling_from_ref); } if let Some(note_from_ref) = note_from_ref { - note = note_from_ref.clone(); + note.clone_from(note_from_ref); } if let Some(stability_from_ref) = stability_from_ref { stability = Some(stability_from_ref.clone()); diff --git a/crates/weaver_schema/src/lib.rs b/crates/weaver_schema/src/lib.rs index 8268ec3c..eb3e2b1b 100644 --- a/crates/weaver_schema/src/lib.rs +++ b/crates/weaver_schema/src/lib.rs @@ -233,7 +233,7 @@ impl TelemetrySchema { } } None => { - self.versions = parent_schema.versions.clone(); + self.versions.clone_from(&parent_schema.versions); } } } diff --git a/justfile b/justfile index e6455bef..c0272841 100644 --- a/justfile +++ b/justfile @@ -8,6 +8,7 @@ install: cargo install cargo-check-external-types pre-push-check: + rustup update cargo update cargo machete cargo fmt --all From 9fe6f0c6aa25412e7bf685e8ae39921e745d3bf6 Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Tue, 12 Mar 2024 12:39:09 -0700 Subject: [PATCH 7/8] feature(template): add a more complex example generating markdown files per group prefix --- .../registry/markdown/groups_per_prefix.md | 50 +++++++++++++++++++ templates/registry/markdown/weaver.yaml | 3 ++ 2 files changed, 53 insertions(+) create mode 100644 templates/registry/markdown/groups_per_prefix.md diff --git a/templates/registry/markdown/groups_per_prefix.md b/templates/registry/markdown/groups_per_prefix.md new file mode 100644 index 00000000..1639ef1c --- /dev/null +++ b/templates/registry/markdown/groups_per_prefix.md @@ -0,0 +1,50 @@ +{%- set file_name = ctx.prefix | file_name -%} +{{- template.set_file_name("prefix/" ~ file_name ~ ".md") -}} + +# Semantic Convention Groups with Prefix `{{ ctx.prefix }}` + +{% for group in ctx.groups %} +## Group `{{ group.id }}` ({{ group.type }}) + +### Brief + +{{ group.brief | trim }} + +prefix: {{ group.prefix }} + +### Attributes + +{% for attribute in group.attributes %} +#### Attribute `{{ attribute.name }}` + +{{ attribute.brief }} + +{% if attribute.note %} +{{ attribute.note | trim }} +{% endif %} + +{%- if attribute.requirement_level == "required" %} +- Requirement Level: Required + {%- elif attribute.requirement_level.conditionally_required %} +- Requirement Level: Conditionally Required - {{ attribute.requirement_level.conditionally_required }} + {%- elif attribute.requirement_level == "recommended" %} +- Requirement Level: Recommended + {%- else %} +- Requirement Level: Optional + {%- endif %} + {% if attribute.tag %} +- Tag: {{ attribute.tag }} + {% endif %} + {%- include "attribute_type.j2" %} + {%- include "examples.j2" -%} + {%- if attribute.sampling_relevant %} +- Sampling relevant: {{ attribute.sampling_relevant }} + {%- endif %} + {%- if attribute.deprecated %} +- Deprecated: {{ attribute.deprecated }} + {%- endif %} + {% if attribute.stability %} +- Stability: {{ attribute.stability | capitalize }} + {% endif %} + {% endfor %} +{% endfor %} diff --git a/templates/registry/markdown/weaver.yaml b/templates/registry/markdown/weaver.yaml index 356ed8e6..b0996d19 100644 --- a/templates/registry/markdown/weaver.yaml +++ b/templates/registry/markdown/weaver.yaml @@ -38,3 +38,6 @@ templates: - pattern: spans.md filter: .groups[] | select(.type == "span") application_mode: single + - pattern: groups_per_prefix.md + filter: '.groups | map(select(.prefix != null and .prefix != "")) | group_by(.prefix) | map({prefix: .[0].prefix, groups: .})' + application_mode: each \ No newline at end of file From 4e3522c8d690ce6ec12ebddcaaef2924e731b6f7 Mon Sep 17 00:00:00 2001 From: Laurent Querel Date: Tue, 12 Mar 2024 15:04:33 -0700 Subject: [PATCH 8/8] feature(resolve): implement registry resolve command --- Cargo.lock | 15 ++--- Cargo.toml | 3 +- README.md | 44 ++++++++++++-- justfile | 1 + src/registry/mod.rs | 23 ++++++-- src/registry/resolve.rs | 125 +++++++++++++++++++++++++++++++++++----- 6 files changed, 179 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e0d0502..a0a6bfd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2695,9 +2695,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.25" +version = "0.11.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" +checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" dependencies = [ "base64", "bytes", @@ -3176,20 +3176,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" -version = "0.6.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ "core-foundation-sys", "libc", @@ -3917,6 +3917,7 @@ dependencies = [ "crossterm", "ratatui", "serde", + "serde_json", "serde_yaml", "tantivy", "tui-textarea", diff --git a/Cargo.toml b/Cargo.toml index c79e12b3..7cba349e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ publish = false serde = { version = "1.0.197", features = ["derive"] } serde_yaml = "0.9.32" serde_json = "1.0.114" -thiserror = "1.0.57" +thiserror = "1.0.58" ureq = "2.9.6" regex = "1.10.3" rayon = "1.9.0" @@ -64,6 +64,7 @@ tantivy = "0.21.1" # workspace dependencies serde.workspace = true serde_yaml.workspace = true +serde_json.workspace = true [package.metadata.cargo-machete] # force cargo machete to ignore the following crates diff --git a/README.md b/README.md index 354fd94a..5bf7df76 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,10 @@ Usage: weaver registry Commands: check Validates a registry (i.e., parsing, resolution of references, extends clauses, and constraints) generate Generates artifacts from a registry - resolve Resolves a registry (not yet implemented) + resolve Resolves a registry search Searches a registry (not yet implemented) stats Calculate and display a set of general statistics on a registry (not yet implemented) update-markdown Update markdown files that contain markers indicating the templates used to update the specified sections - help Print this message or the help of the given subcommand(s) Options: -h, --help Print help @@ -86,8 +85,6 @@ Options: Local path or Git URL of the semantic convention registry to check [default: https://github.com/open-telemetry/semantic-conventions.git] -d, --registry-git-sub-dir Optional path in the Git repository where the semantic convention registry is located [default: model] - -h, --help - Print help ``` ### Sub-Command `registry generate` @@ -108,8 +105,43 @@ Options: Local path or Git URL of the semantic convention registry [default: https://github.com/open-telemetry/semantic-conventions.git] -d, --registry-git-sub-dir Optional path in the Git repository where the semantic convention registry is located [default: model] - -h, --help - Print help +``` + +### Sub-Command `registry resolve` + +``` +Resolves a registry + +Usage: weaver registry resolve [OPTIONS] + +Options: + -r, --registry + Local path or Git URL of the semantic convention registry + + [default: https://github.com/open-telemetry/semantic-conventions.git] + + -d, --registry-git-sub-dir + Optional path in the Git repository where the semantic convention registry is located + + [default: model] + + --catalog + Flag to indicate if the shared catalog should be included in the resolved schema + + --lineage + Flag to indicate if lineage information should be included in the resolved schema (not yet implemented) + + -o, --output + Output file to write the resolved schema to If not specified, the resolved schema is printed to stdout + + -f, --format + Output format for the resolved schema If not specified, the resolved schema is printed in YAML format Supported formats: yaml, json Default format: yaml Example: `--format json` + + [default: yaml] + + Possible values: + - yaml: YAML format + - json: JSON format ``` ### Crates Layout diff --git a/justfile b/justfile index c0272841..c8965b8e 100644 --- a/justfile +++ b/justfile @@ -9,6 +9,7 @@ install: pre-push-check: rustup update + cargo clean cargo update cargo machete cargo fmt --all diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 72629268..062fc23f 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -36,7 +36,7 @@ pub enum RegistrySubCommand { Check(RegistryCheckArgs), /// Generates artifacts from a registry. Generate(RegistryGenerateArgs), - /// Resolves a registry (not yet implemented). + /// Resolves a registry. Resolve(RegistryResolveArgs), /// Searches a registry (not yet implemented). Search(RegistrySearchArgs), @@ -46,6 +46,23 @@ pub enum RegistrySubCommand { UpdateMarkdown(RegistryUpdateMarkdownArgs), } +/// Set of parameters used to specify a semantic convention registry. +#[derive(Args, Debug)] +pub struct RegistryArgs { + /// Local path or Git URL of the semantic convention registry. + #[arg( + short = 'r', + long, + default_value = "https://github.com/open-telemetry/semantic-conventions.git" + )] + pub registry: String, + + /// Optional path in the Git repository where the semantic convention + /// registry is located + #[arg(short = 'd', long, default_value = "model")] + pub registry_git_sub_dir: Option, +} + /// Manage a semantic convention registry. pub fn semconv_registry(log: impl Logger + Sync + Clone, command: &RegistryCommand) { let cache = Cache::try_new().unwrap_or_else(|e| { @@ -57,9 +74,7 @@ pub fn semconv_registry(log: impl Logger + Sync + Clone, command: &RegistryComma RegistrySubCommand::Check(args) => check::command(log, &cache, args), RegistrySubCommand::Generate(args) => generate::command(log, &cache, args), RegistrySubCommand::Stats(args) => stats::command(log, &cache, args), - RegistrySubCommand::Resolve(_) => { - unimplemented!() - } + RegistrySubCommand::Resolve(args) => resolve::command(log, &cache, args), RegistrySubCommand::Search(_) => { unimplemented!() } diff --git a/src/registry/resolve.rs b/src/registry/resolve.rs index 9c60fddd..78f59491 100644 --- a/src/registry/resolve.rs +++ b/src/registry/resolve.rs @@ -2,26 +2,123 @@ //! Resolve a semantic convention registry. -use clap::Args; use std::path::PathBuf; +use clap::{Args, ValueEnum}; +use serde::Serialize; + +use weaver_cache::Cache; +use weaver_forge::registry::TemplateRegistry; +use weaver_logger::Logger; +use weaver_resolver::SchemaResolver; + +use crate::registry::RegistryArgs; + +/// Supported output formats for the resolved schema +#[derive(Debug, Clone, ValueEnum)] +enum Format { + /// YAML format + Yaml, + /// JSON format + Json, +} + /// Parameters for the `registry resolve` sub-command #[derive(Debug, Args)] pub struct RegistryResolveArgs { - /// Local path or Git URL of the semantic convention registry. - #[arg( - short = 'r', - long, - default_value = "https://github.com/open-telemetry/semantic-conventions.git" - )] - pub registry: String, - - /// Optional path in the Git repository where the semantic convention - /// registry is located - #[arg(short = 'd', long, default_value = "model")] - pub registry_git_sub_dir: Option, + /// Parameters to specify the semantic convention registry + #[command(flatten)] + registry: RegistryArgs, + + /// Flag to indicate if the shared catalog should be included in the resolved schema + #[arg(long, default_value = "false")] + catalog: bool, + + /// Flag to indicate if lineage information should be included in the + /// resolved schema (not yet implemented) + #[arg(long, default_value = "false")] + lineage: bool, /// Output file to write the resolved schema to /// If not specified, the resolved schema is printed to stdout - pub output: Option, + #[arg(short, long)] + output: Option, + + /// Output format for the resolved schema + /// If not specified, the resolved schema is printed in YAML format + /// Supported formats: yaml, json + /// Default format: yaml + /// Example: `--format json` + #[arg(short, long, default_value = "yaml")] + format: Format, +} + +/// Resolve a semantic convention registry and write the resolved schema to a +/// file or print it to stdout. +pub(crate) fn command( + logger: impl Logger + Sync + Clone, + cache: &Cache, + args: &RegistryResolveArgs, +) { + logger.loading(&format!("Resolving registry `{}`", args.registry.registry)); + + // Load the semantic convention registry into a local cache. + let mut registry = SchemaResolver::load_semconv_registry( + args.registry.registry.to_string(), + args.registry.registry_git_sub_dir.clone(), + cache, + logger.clone(), + ) + .unwrap_or_else(|e| { + panic!("Failed to load and parse the semantic convention registry, error: {e}"); + }); + + // Resolve the semantic convention registry. + let schema = + SchemaResolver::resolve_semantic_convention_registry(&mut registry, logger.clone()) + .expect("Failed to resolve registry"); + + // Serialize the resolved schema and write it + // to a file or print it to stdout. + match args.catalog { + // The original resolved schema already includes the catalog. + // So, we just need to serialize it. + true => apply_format(&args.format, &schema) + .map_err(|e| format!("Failed to serialize the registry: {e:?}")), + // Build a template registry from the resolved schema and serialize it. + // The template registry does not include any reference to a shared + // catalog of attributes. + false => { + let registry = TemplateRegistry::try_from_resolved_registry( + &schema.registries[0], + &schema.catalog, + ) + .unwrap_or_else(|e| panic!("Failed to create the registry without catalog: {e:?}")); + apply_format(&args.format, ®istry) + .map_err(|e| format!("Failed to serialize the registry: {e:?}")) + } + } + .and_then(|s| match args.output { + // Write the resolved registry to a file. + Some(ref path) => std::fs::write(path, s) + .map_err(|e| format!("Failed to write the resolved registry to file: {e:?}")), + // Print the resolved registry to stdout. + None => { + println!("{}", s); + Ok(()) + } + }) + .unwrap_or_else(|e| { + // Capture all the errors + panic!("{}", e); + }); +} + +fn apply_format(format: &Format, object: &T) -> Result { + match format { + Format::Yaml => serde_yaml::to_string(object) + .map_err(|e| format!("Failed to serialize in Yaml the resolved registry: {:?}", e)), + Format::Json => serde_json::to_string_pretty(object) + .map_err(|e| format!("Failed to serialize in Json the resolved registry: {:?}", e)), + } }