#![doc = include_str!("../README.md")]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, rust_2018_idioms)]
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate, clippy::missing_errors_doc)]
#![forbid(unsafe_code)]
use std::convert::TryFrom;
use std::fs::File;
use std::io;
use std::io::Write;
use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use clap::{crate_description, crate_name, crate_version, Arg, ArgMatches, Command};
use serde::Serialize;
use companion::CompanionData;
use config::PathStyle;
use config::{Config, OutputFormat};
use daml::json_api::schema_encoder::{
DataDict, JsonSchemaEncoder, ReferenceMode, RenderDescription, RenderSchema, RenderTitle, SchemaEncoderConfig,
};
use daml::lf::DarFile;
use filter::{TemplateFilter, TemplateFilterInput};
use oas::OpenAPI;
use oas::OpenAPIEncoder;
use crate::a2s::AsyncAPI;
use crate::a2s::AsyncAPIEncoder;
use log::LevelFilter;
use serde::de::DeserializeOwned;
use simple_logger::SimpleLogger;
#[doc(hidden)]
mod a2s;
#[doc(hidden)]
mod choice_event_extractor;
#[doc(hidden)]
mod common;
#[doc(hidden)]
mod companion;
#[doc(hidden)]
mod component_encoder;
#[doc(hidden)]
mod config;
#[doc(hidden)]
mod data_searcher;
#[doc(hidden)]
mod filter;
#[doc(hidden)]
mod format;
#[doc(hidden)]
mod json_api_schema;
#[doc(hidden)]
mod oas;
#[doc(hidden)]
mod schema;
#[doc(hidden)]
mod util;
#[doc(hidden)]
const DEFAULT_DATA_DICT_FILE: &str = ".datadict.yaml";
#[doc(hidden)]
const DEFAULT_TEMPLATE_FILTER_FILE: &str = ".template_filter.yaml";
#[doc(hidden)]
const DEFAULT_COMPANION_FILE: &str = ".companion.yaml";
#[doc(hidden)]
fn main() -> Result<()> {
let oas = Command::new("oas")
.about("Generate an OpenAPI document from the given Dar file")
.arg(make_dar_arg())
.arg(make_log_level_arg())
.arg(make_format_arg())
.arg(make_output_arg())
.arg(make_companion_file_arg())
.arg(make_datadict_file_arg())
.arg(make_template_filter_file_arg())
.arg(make_module_path_arg())
.arg(make_data_title_arg())
.arg(make_type_description_arg())
.arg(make_reference_prefix_arg())
.arg(make_reference_mode_arg())
.arg(make_include_package_id_arg())
.arg(make_include_archive_choice_arg())
.arg(make_include_general_operations_arg())
.arg(make_path_style_arg());
let a2s = Command::new("a2s")
.about("Generate an AsyncAPI document from the given Dar file")
.arg(make_dar_arg())
.arg(make_log_level_arg())
.arg(make_format_arg())
.arg(make_output_arg())
.arg(make_companion_file_arg())
.arg(make_datadict_file_arg())
.arg(make_template_filter_file_arg())
.arg(make_module_path_arg())
.arg(make_data_title_arg())
.arg(make_type_description_arg())
.arg(make_reference_prefix_arg())
.arg(make_reference_mode_arg())
.arg(make_include_package_id_arg());
let matches = Command::new(crate_name!())
.version(crate_version!())
.about(crate_description!())
.arg_required_else_help(true)
.subcommand(oas)
.subcommand(a2s)
.get_matches();
match matches.subcommand() {
Some(("oas", sub)) => execute_oas(&parse_config(sub))?,
Some(("a2s", sub)) => execute_a2s(&parse_config(sub))?,
_ => {},
};
Ok(())
}
#[doc(hidden)]
fn make_dar_arg() -> Arg<'static> {
Arg::new("dar").help("Sets the input dar file to use").required(true).index(1)
}
#[doc(hidden)]
fn make_log_level_arg() -> Arg<'static> {
Arg::new("v").required(false).short('v').multiple_occurrences(true).help("Sets the level of verbosity")
}
#[doc(hidden)]
fn make_format_arg() -> Arg<'static> {
Arg::new("format")
.short('f')
.long("format")
.takes_value(true)
.possible_values(&["json", "yaml"])
.default_value("json")
.required(false)
.help("the output format")
}
#[doc(hidden)]
fn make_output_arg() -> Arg<'static> {
Arg::new("output").short('o').long("output").takes_value(true).required(false).help("the output file path")
}
#[doc(hidden)]
fn make_companion_file_arg() -> Arg<'static> {
Arg::new("companion-file")
.short('c')
.long("companion-file")
.takes_value(true)
.required(false)
.help("the companion yaml file with auxiliary data to inject into the generated OAS document")
}
#[doc(hidden)]
fn make_datadict_file_arg() -> Arg<'static> {
Arg::new("datadict-file")
.short('d')
.long("datadict-file")
.takes_value(true)
.required(false)
.help("the data dictionary to use to augment the generated JSON schema")
}
#[doc(hidden)]
fn make_template_filter_file_arg() -> Arg<'static> {
Arg::new("template-filter-file")
.short('t')
.long("template-filter-file")
.takes_value(true)
.required(false)
.help("the template filter to apply")
}
#[doc(hidden)]
fn make_module_path_arg() -> Arg<'static> {
Arg::new("module-path")
.short('m')
.long("module")
.takes_value(true)
.required(false)
.help("module path prefix in the form Foo.Bar.Baz")
}
#[doc(hidden)]
fn make_data_title_arg() -> Arg<'static> {
Arg::new("data-title")
.long("data-title")
.takes_value(true)
.possible_values(&["none", "data"])
.default_value("data")
.required(false)
.help("include the `title` property describing the data item name (i.e. Foo.Bar:Baz)")
}
#[doc(hidden)]
fn make_type_description_arg() -> Arg<'static> {
Arg::new("type-description")
.long("type-description")
.takes_value(true)
.possible_values(&["none", "data", "all"])
.default_value("all")
.required(false)
.help("include the `description` property describing the Daml type")
}
#[doc(hidden)]
fn make_reference_prefix_arg() -> Arg<'static> {
Arg::new("reference-prefix")
.short('p')
.long("reference-prefix")
.takes_value(true)
.default_value("#/components/schemas/")
.required(false)
.help("the prefix for absolute $ref schema references")
}
#[doc(hidden)]
fn make_reference_mode_arg() -> Arg<'static> {
Arg::new("reference-mode")
.short('r')
.long("reference-mode")
.takes_value(true)
.possible_values(&["ref", "inline"])
.default_value("ref")
.required(false)
.help("encode references as as $ref schema links or inline")
}
#[doc(hidden)]
fn make_include_package_id_arg() -> Arg<'static> {
Arg::new("include-package-id")
.long("include-package-id")
.required(false)
.help("include the package id in fully qualified templates")
}
#[doc(hidden)]
fn make_include_archive_choice_arg() -> Arg<'static> {
Arg::new("include-archive-choice")
.long("include-archive-choice")
.required(false)
.help("include the Archive choice which is available on every template")
}
#[doc(hidden)]
fn make_include_general_operations_arg() -> Arg<'static> {
Arg::new("include-general-operations").long("include-general-operations").required(false).help(
"include the general (non-template specific) /v1/create, /v1/exercise, /v1/create-and-exercise & /v1/fetch \
endpoints",
)
}
#[doc(hidden)]
fn make_path_style_arg() -> Arg<'static> {
Arg::new("path-style")
.short('s')
.long("path-style")
.takes_value(true)
.possible_values(&["fragment", "slash"])
.default_value("fragment")
.required(false)
.help("encode paths with fragment (i.e. '#') or slash ('/')")
}
#[doc(hidden)]
fn parse_config(matches: &ArgMatches) -> Config<'_> {
let dar_file = matches.value_of("dar").unwrap().to_string();
let level_filter = match matches.occurrences_of("v") {
0 => LevelFilter::Off,
1 => LevelFilter::Info,
_ => LevelFilter::Debug,
};
let companion_file = matches.value_of("companion-file").map(ToString::to_string);
let data_dict_file = matches.value_of("datadict-file").map(ToString::to_string);
let template_filter_file = matches.value_of("template-filter-file").map(ToString::to_string);
let format = match matches.value_of("format") {
None => OutputFormat::Json,
Some(s) if s == "json" => OutputFormat::Json,
Some(s) if s == "yaml" => OutputFormat::Yaml,
Some(s) => panic!("unknown format {}", s),
};
let output_file = matches.value_of("output").map(ToString::to_string);
let module_path = matches.value_of("module-path").map(|v| v.split('.').collect::<Vec<_>>()).unwrap_or_default();
let render_title = match matches.value_of("data-title") {
None => RenderTitle::None,
Some(s) if s == "none" => RenderTitle::None,
Some(s) if s == "data" => RenderTitle::Data,
Some(s) => panic!("unknown data-title {}", s),
};
let render_description = match matches.value_of("type-description") {
None => RenderDescription::None,
Some(s) if s == "none" => RenderDescription::None,
Some(s) if s == "data" => RenderDescription::Data,
Some(s) if s == "all" => RenderDescription::All,
Some(s) => panic!("unknown type-description {}", s),
};
let reference_prefix = matches.value_of("reference-prefix").unwrap();
let reference_mode = match matches.value_of("reference-mode") {
None => ReferenceMode::default(),
Some(s) if s == "ref" => ReferenceMode::Reference {
prefix: reference_prefix.to_string(),
},
Some(s) if s == "inline" => ReferenceMode::Inline,
Some(s) => panic!("unknown reference-prefix {}", s),
};
let emit_package_id = matches.is_present("include-package-id");
let include_archive_choice = matches.is_present("include-archive-choice");
let include_general_operations = matches.is_present("include-general-operations");
let path_style = match matches.value_of("path-style") {
None => PathStyle::default(),
Some(s) if s == "fragment" => PathStyle::Fragment,
Some(s) if s == "slash" => PathStyle::Slash,
Some(s) => panic!("unknown path-style {}", s),
};
Config {
dar_file,
companion_file,
data_dict_file,
template_filter_file,
format,
output_file,
module_path,
render_title,
render_description,
reference_prefix,
reference_mode,
emit_package_id,
include_archive_choice,
include_general_operations,
path_style,
level_filter,
}
}
#[doc(hidden)]
fn execute_oas(config: &Config<'_>) -> Result<()> {
SimpleLogger::new().with_level(config.level_filter).init().unwrap();
log::info!("Generating OAS specification documents for {}", config.dar_file);
log::info!("Loading dar file...");
let dar = DarFile::from_file(&config.dar_file).context(format!("dar file not found: {}", &config.dar_file))?;
log::info!("Loading companion data file...");
let companion_data = get_companion_data(&config.companion_file)?;
log::info!("Loading data dict file...");
let data_dict = get_data_dict(&config.data_dict_file)?;
log::info!("Loading template filter file...");
let template_filter = get_template_filter(&config.template_filter_file)?;
log::info!("Generating API document...");
let oas = generate_openapi(&dar, config, &companion_data, data_dict, &template_filter)?;
log::info!("Rendering API document...");
let rendered = render(&oas, config.format)?;
log::info!("Writing API document...");
write_document(&rendered, config.output_file.as_deref())
}
#[doc(hidden)]
fn generate_openapi(
dar_file: &DarFile,
config: &Config<'_>,
companion_data: &CompanionData,
data_dict: DataDict,
template_filter: &TemplateFilter,
) -> Result<OpenAPI> {
let encoder_config = SchemaEncoderConfig::new(
RenderSchema::None,
config.render_title,
config.render_description,
config.reference_mode.clone(),
data_dict,
);
dar_file.apply(|archive| {
let encoder = JsonSchemaEncoder::new_with_config(archive, encoder_config);
let generator = OpenAPIEncoder::new(
archive,
&config.module_path,
template_filter,
config.reference_prefix,
config.emit_package_id,
config.include_archive_choice,
config.include_general_operations,
config.path_style,
companion_data,
encoder,
);
generator.encode_archive()
})?
}
#[doc(hidden)]
fn execute_a2s(config: &Config<'_>) -> Result<()> {
SimpleLogger::new().with_level(config.level_filter).init().unwrap();
log::info!("Generating A2S specification documents for {}", config.dar_file);
let dar = DarFile::from_file(&config.dar_file).context(format!("dar file not found: {}", &config.dar_file))?;
let companion_data = get_companion_data(&config.companion_file)?;
let data_dict = get_data_dict(&config.data_dict_file)?;
let template_filter = get_template_filter(&config.template_filter_file)?;
let a2s = generate_asyncapi(&dar, config, &companion_data, data_dict, &template_filter)?;
write_document(&render(&a2s, config.format)?, config.output_file.as_deref())
}
#[doc(hidden)]
fn generate_asyncapi(
dar_file: &DarFile,
config: &Config<'_>,
companion_data: &CompanionData,
data_dict: DataDict,
template_filter: &TemplateFilter,
) -> Result<AsyncAPI> {
let encoder_config = SchemaEncoderConfig::new(
RenderSchema::None,
config.render_title,
config.render_description,
config.reference_mode.clone(),
data_dict,
);
dar_file.apply(|archive| {
let encoder = JsonSchemaEncoder::new_with_config(archive, encoder_config);
let generator = AsyncAPIEncoder::new(
archive,
&config.module_path,
template_filter,
config.reference_prefix,
config.emit_package_id,
companion_data,
encoder,
);
generator.encode_archive()
})?
}
#[doc(hidden)]
fn get_companion_data(filter_file_name: &Option<String>) -> Result<CompanionData> {
read_file(filter_file_name, DEFAULT_COMPANION_FILE)
.map_err(|err| anyhow!("failed to parse companion file").context(err))
}
#[doc(hidden)]
fn get_data_dict(data_dict_file_name: &Option<String>) -> Result<DataDict> {
read_file(data_dict_file_name, DEFAULT_DATA_DICT_FILE)
.map_err(|err| anyhow!("failed to parse datadict file").context(err))
}
#[doc(hidden)]
fn get_template_filter(filter_file_name: &Option<String>) -> Result<TemplateFilter> {
let filter: TemplateFilterInput = read_file(filter_file_name, DEFAULT_TEMPLATE_FILTER_FILE)?;
TemplateFilter::try_from(filter).map_err(|err| anyhow!("failed to parse template filter file").context(err))
}
#[doc(hidden)]
fn read_file<T: DeserializeOwned + Default, S: AsRef<str>>(file_name: &Option<String>, fallback: S) -> Result<T> {
let path = file_name.as_ref();
if let Some(name) = path {
let path = PathBuf::from(name);
if path.is_file() && path.exists() {
let f = std::fs::File::open(path)?;
Ok(serde_yaml::from_reader(f).map_err(|err| anyhow!("failed to parse file {}", name).context(err))?)
} else {
Err(anyhow!(format!("file {} not found", path.display())))
}
} else {
let path = PathBuf::from(fallback.as_ref());
if path.is_file() && path.exists() {
let f = std::fs::File::open(path)?;
Ok(serde_yaml::from_reader(f)
.map_err(|err| anyhow!("failed to parse file {}", fallback.as_ref()).context(err))?)
} else {
Ok(T::default())
}
}
}
#[doc(hidden)]
fn render<S: Serialize>(doc: &S, format: OutputFormat) -> Result<String> {
match format {
OutputFormat::Json => Ok(serde_json::to_string_pretty(&doc)?),
OutputFormat::Yaml => {
let json_string = serde_json::to_string(&doc)?;
let yaml_value: serde_yaml::Value = serde_yaml::from_str(&json_string)?;
Ok(serde_yaml::to_string(&yaml_value)?)
},
}
}
#[doc(hidden)]
fn write_document(doc: &str, path: Option<&str>) -> Result<()> {
if let Some(path) = path {
let target = PathBuf::from(path);
let mut file = File::create(target)?;
Ok(file.write_all(doc.as_bytes())?)
} else {
Ok(io::stdout().write_all(doc.as_bytes())?)
}
}