use crate::args::Generate;
use crate::args::shadow::PKG_VERSION;
use crate::command::server::{
PrepareDirsParams, RuntimeConfigAvailability, VerifyParams, create_engines,
deployment_compile_link, deployment_verify_config, prepare_dirs, server_verify,
};
use crate::command::termination_notifier::termination_notifier;
use crate::config::config_holder::{
ConfigHolder, OBELISK_HELP_DEPLOYMENT_TOML, load_deployment_validated, server_config_template,
};
use crate::config::secret_registry::SecretRegistry;
use crate::config::toml::{
ActivityExternalComponentConfigToml, ActivityStubComponentConfigToml, ComponentLocationToml,
DeploymentTomlValidated, JsLocationToml, ServerConfigToml,
};
use crate::init::{self};
use crate::project_dirs;
use anyhow::Context;
use concepts::{ComponentType, ExecutionId, PackageIfcFns, PkgFqn, prefixed_ulid::DeploymentId};
use directories::{BaseDirs, ProjectDirs};
use hashbrown::{HashMap, HashSet};
use serde::Serialize;
use std::{borrow::Cow, path::PathBuf, sync::Arc};
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt as _;
use tokio::sync::watch;
use utils::{wasm_tools::WasmComponent, wit};
use wasm_workers::registry::WitOrigin;
impl Generate {
pub(crate) async fn run(
self,
secret_registry: Arc<SecretRegistry>,
) -> Result<(), anyhow::Error> {
match self {
Generate::ServerConfig {
json,
trusted,
output,
force,
} => {
if let Some(output) = output {
let config_file =
ConfigHolder::generate_server_config(output, trusted, force).await?;
let result = GeneratedPathStatus {
path: config_file,
status: "generated",
};
print_generated_path_statuses(&[result], json)?;
} else {
print!("{}", server_config_template(trusted));
}
Ok(())
}
Generate::Deployment {
json,
output,
force,
} => {
if let Some(output) = output {
let config_file =
ConfigHolder::generate_default_deployment_config(output, force).await?;
let result = GeneratedPathStatus {
path: config_file,
status: "generated",
};
print_generated_path_statuses(&[result], json)?;
} else {
print!("{OBELISK_HELP_DEPLOYMENT_TOML}");
}
Ok(())
}
Generate::WitExtensions {
json,
component_type,
input_wit_directory,
output_directory,
force,
} => {
let results = generate_exported_extension_wits(
input_wit_directory,
output_directory,
component_type,
force,
)
.await?;
print_generated_path_statuses(&results, json)?;
Ok(())
}
Generate::WitSupport {
json,
component_type,
output_directory,
force,
} => {
let results =
generate_support_wits(component_type, output_directory, force).await?;
print_generated_path_statuses(&results, json)?;
Ok(())
}
Generate::WitDeps {
json,
deployment,
output_directory,
force,
skip_local,
} => {
let results = generate_wit_deps(
project_dirs(),
BaseDirs::new(),
deployment,
output_directory,
force,
skip_local,
secret_registry,
)
.await?;
print_generated_path_statuses(&results, json)?;
Ok(())
}
Generate::ExecutionId { json } => {
let execution_id = ExecutionId::generate();
if json {
println!("{}", serde_json::to_string_pretty(&execution_id)?);
} else {
println!("{execution_id}");
}
Ok(())
}
Generate::DeploymentId { json } => {
let deployment_id = DeploymentId::generate();
if json {
println!("{}", serde_json::to_string_pretty(&deployment_id)?);
} else {
println!("{deployment_id}");
}
Ok(())
}
Generate::Token {
json,
server_config,
} => {
let token = crate::api::generate_token();
let hash = crate::api::token_hash(&token);
if let Some(server_config) = &server_config {
let contents = tokio::fs::read_to_string(server_config)
.await
.with_context(|| format!("cannot read {server_config:?}"))?;
let contents = add_token_hash(&contents, &hash)
.with_context(|| format!("cannot update {server_config:?}"))?;
tokio::fs::write(server_config, contents)
.await
.with_context(|| format!("cannot write {server_config:?}"))?;
eprintln!("Added {hash} to `api.token_hashes` in {server_config:?}");
}
if json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"token": token,
"hash": hash.to_string(),
}))?
);
} else {
println!("{token}");
if server_config.is_none()
&& std::io::IsTerminal::is_terminal(&std::io::stdout())
{
eprintln!(
"Add to server.toml (or rerun with --server-config <server.toml>):"
);
eprintln!("api.token_hashes = [\"{hash}\"]");
}
}
Ok(())
}
Generate::Prompt { description } => {
let version = format!("v{PKG_VERSION}");
let description = description.join(" ");
println!(
"Fetch https://obeli.sk/docs/{version}/llms.txt for the full Obelisk reference. Task:\n{description}"
);
Ok(())
}
}
}
}
fn add_token_hash(
server_config_contents: &str,
hash: &concepts::component_id::Digest,
) -> Result<String, anyhow::Error> {
use toml_edit::{DocumentMut, Item, Table};
let mut doc = server_config_contents
.parse::<DocumentMut>()
.context("cannot parse server config as TOML")?;
let api = doc
.entry("api")
.or_insert(Item::Table(Table::new()))
.as_table_like_mut()
.context("`api` must be a table")?;
let hashes = api
.entry("token_hashes")
.or_insert(Item::Value(
toml_edit::Value::Array(toml_edit::Array::new()),
))
.as_array_mut()
.context("`api.token_hashes` must be an array")?;
hashes.push(hash.to_string());
Ok(doc.to_string())
}
#[derive(Debug, Serialize)]
struct GeneratedPathStatus {
path: PathBuf,
status: &'static str,
}
fn print_generated_path_statuses(
results: &[GeneratedPathStatus],
json: bool,
) -> Result<(), anyhow::Error> {
if json {
println!("{}", serde_json::to_string_pretty(results)?);
} else {
for result in results {
match result.status {
"generated" => println!("Generated {:?}", result.path),
"created_or_updated" => println!("{:?} created or updated", result.path),
"up_to_date" => println!("{:?} is up to date", result.path),
"written" => println!("{:?} written", result.path),
status => println!("{:?} {status}", result.path),
}
}
}
Ok(())
}
#[cfg(test)]
fn write_schema<T: schemars::JsonSchema>(output: Option<PathBuf>) -> Result<(), anyhow::Error> {
use std::{
fs::File,
io::{BufWriter, Write as _, stdout},
};
let schema = schemars::schema_for!(T);
if let Some(output) = output {
let mut writer = BufWriter::new(File::create(&output)?);
serde_json::to_writer_pretty(&mut writer, &schema)?;
writer.write_all(b"\n")?;
writer.flush()?;
} else {
serde_json::to_writer_pretty(stdout().lock(), &schema)?;
}
Ok(())
}
#[cfg(test)]
fn generate_server_config_schema(output: Option<PathBuf>) -> Result<(), anyhow::Error> {
write_schema::<crate::config::toml::ServerConfigToml>(output)
}
#[cfg(test)]
fn generate_deployment_schema(output: Option<PathBuf>) -> Result<(), anyhow::Error> {
write_schema::<crate::config::toml::DeploymentToml>(output)
}
#[cfg(test)]
fn generate_db_schema(output: Option<PathBuf>) -> Result<(), anyhow::Error> {
use std::{
fs::File,
io::{BufWriter, Write as _, stdout},
};
let schema = schemars::schema_for!(concepts::storage::DbStorageSchema);
if let Some(output) = output {
let mut writer = BufWriter::new(File::create(&output)?);
serde_json::to_writer_pretty(&mut writer, &schema)?;
writer.write_all(b"\n")?;
writer.flush()?;
} else {
serde_json::to_writer_pretty(stdout().lock(), &schema)?;
}
Ok(())
}
#[cfg(test)]
fn generate_openapi_schema(output: Option<PathBuf>) -> Result<(), anyhow::Error> {
use std::{
fs::File,
io::{BufWriter, Write as _, stdout},
};
use utoipa::OpenApi as _;
let schema = crate::server::web_api_server::ApiDoc::openapi();
if let Some(output) = output {
let mut writer = BufWriter::new(File::create(&output)?);
serde_json::to_writer_pretty(&mut writer, &schema)?;
writer.write_all(b"\n")?;
writer.flush()?;
} else {
serde_json::to_writer_pretty(stdout().lock(), &schema)?;
println!();
}
Ok(())
}
#[cfg(test)]
fn generate_component_metadata_annotation_schema(
output: Option<PathBuf>,
) -> Result<(), anyhow::Error> {
write_schema::<crate::oci::ComponentMetadataAnnotation>(output)
}
#[cfg(test)]
#[derive(Debug, Serialize)]
struct CliCommandSchema {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
about: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
options: Vec<CliArgSchema>,
#[serde(skip_serializing_if = "Vec::is_empty")]
positionals: Vec<CliArgSchema>,
#[serde(skip_serializing_if = "Vec::is_empty")]
subcommands: Vec<CliCommandSchema>,
}
#[cfg(test)]
#[derive(Debug, Serialize)]
struct CliArgSchema {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
short: Option<char>,
#[serde(skip_serializing_if = "Option::is_none")]
value_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
help: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
accepts: Option<CliArgAcceptsSchema>,
}
#[cfg(test)]
#[derive(Debug, Serialize)]
struct CliArgAcceptsSchema {
#[serde(skip_serializing_if = "Option::is_none")]
one_of: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
many: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<bool>,
}
#[cfg(test)]
fn generate_cli_schema(output: Option<PathBuf>) -> Result<(), anyhow::Error> {
let command = <crate::args::Args as clap::CommandFactory>::command();
let schema = command_to_schema(&command);
write_json(output, &schema)
}
#[cfg(test)]
fn command_to_schema(command: &clap::Command) -> CliCommandSchema {
let mut options = Vec::new();
let mut positionals = Vec::new();
for arg in command.get_arguments() {
if skip_arg(arg) {
continue;
}
let schema = arg_to_schema(arg);
if arg.is_positional() {
positionals.push(schema);
} else {
options.push(schema);
}
}
CliCommandSchema {
name: command.get_name().to_string(),
about: command_about(command),
options,
positionals,
subcommands: command.get_subcommands().map(command_to_schema).collect(),
}
}
#[cfg(test)]
fn arg_to_schema(arg: &clap::Arg) -> CliArgSchema {
CliArgSchema {
name: arg_name(arg),
short: arg.get_short(),
value_name: arg_value_name(arg),
help: arg_help(arg),
required: arg.is_required_set(),
accepts: arg_accepts(arg),
}
}
#[cfg(test)]
fn command_about(command: &clap::Command) -> Option<String> {
command
.get_about()
.map(ToString::to_string)
.filter(|about| !about.trim().is_empty())
}
#[cfg(test)]
fn arg_help(arg: &clap::Arg) -> Option<String> {
arg.get_help()
.map(ToString::to_string)
.filter(|help| !help.trim().is_empty())
}
#[cfg(test)]
fn arg_name(arg: &clap::Arg) -> String {
if let Some(long) = arg.get_long() {
format!("--{long}")
} else {
arg.get_id().to_string()
}
}
#[cfg(test)]
fn arg_value_name(arg: &clap::Arg) -> Option<String> {
arg.get_num_args()
.filter(clap::builder::ValueRange::takes_values)
.and_then(|_| arg.get_value_names())
.and_then(|names| names.first())
.map(ToString::to_string)
}
#[cfg(test)]
fn arg_accepts(arg: &clap::Arg) -> Option<CliArgAcceptsSchema> {
let choices: Vec<String> = arg
.get_possible_values()
.into_iter()
.filter(|value| !value.is_hide_set())
.map(|value| value.get_name().to_string())
.filter(|value| value != "true" && value != "false")
.collect();
let one_of = (!choices.is_empty()).then_some(choices);
let many = arg.get_num_args().and_then(|range| {
let max = range.max_values();
((range.min_values() > 1) || max > 1 || max == usize::MAX).then_some(true)
});
let path = matches!(
arg.get_value_hint(),
clap::ValueHint::AnyPath
| clap::ValueHint::FilePath
| clap::ValueHint::DirPath
| clap::ValueHint::ExecutablePath
)
.then_some(true);
if one_of.is_none() && many.is_none() && path.is_none() {
None
} else {
Some(CliArgAcceptsSchema { one_of, many, path })
}
}
#[cfg(test)]
fn skip_arg(arg: &clap::Arg) -> bool {
matches!(
arg.get_action(),
clap::ArgAction::Help | clap::ArgAction::HelpShort | clap::ArgAction::HelpLong
)
}
#[cfg(test)]
fn write_json<T: serde::Serialize>(
output: Option<PathBuf>,
value: &T,
) -> Result<(), anyhow::Error> {
use std::{
fs::File,
io::{BufWriter, Write as _, stdout},
};
if let Some(output) = output {
let mut writer = BufWriter::new(File::create(&output)?);
serde_json::to_writer_pretty(&mut writer, value)?;
writer.write_all(b"\n")?;
writer.flush()?;
} else {
serde_json::to_writer_pretty(stdout().lock(), value)?;
println!();
}
Ok(())
}
pub(crate) const OBELISK_WIT_HEADER: &str = "// Generated by Obelisk";
async fn generate_exported_extension_wits(
input_wit_directory: PathBuf,
output_directory: PathBuf,
component_type: ComponentType,
force: bool,
) -> Result<Vec<GeneratedPathStatus>, anyhow::Error> {
let wasm_component = WasmComponent::new_from_wit_folder(&input_wit_directory, component_type)?;
let pkgs_to_wits = wasm_component.exported_extension_wits()?;
let mut results = Vec::new();
for (pkg_fqn, new_content) in pkgs_to_wits {
let pkg_file_name = pkg_fqn.as_file_name();
let pkg_folder = output_directory.join(&pkg_file_name);
let wit_file = pkg_folder.join(format!("{pkg_file_name}.wit"));
let old_content = tokio::fs::read_to_string(&wit_file)
.await
.unwrap_or_default();
let old_content = if force {
None
} else {
Some(strip_header(&old_content))
};
if old_content.as_ref() != Some(&new_content) {
let new_content = format!("{OBELISK_WIT_HEADER} {PKG_VERSION}\n{new_content}");
tokio::fs::create_dir_all(&pkg_folder)
.await
.with_context(|| format!("cannot write {pkg_folder:?}"))?;
tokio::fs::write(&wit_file, new_content.as_bytes())
.await
.with_context(|| format!("cannot write {wit_file:?}"))?;
results.push(GeneratedPathStatus {
path: wit_file,
status: "created_or_updated",
});
} else {
results.push(GeneratedPathStatus {
path: wit_file,
status: "up_to_date",
});
}
}
Ok(results)
}
fn strip_header(old_content: &str) -> String {
let old_content = match old_content.strip_prefix(OBELISK_WIT_HEADER) {
Some(wit) => {
if let Some((_, wit)) = wit.split_once('\n') {
Cow::Borrowed(wit)
} else {
Cow::Borrowed(wit)
}
}
None => Cow::Borrowed(old_content),
};
let old_content = match old_content.strip_prefix(&format!("/{OBELISK_WIT_HEADER}")) {
Some(wit) => {
if let Some((_, wit)) = wit.split_once('\n') {
Cow::Borrowed(wit)
} else {
Cow::Borrowed(wit)
}
}
None => old_content,
};
old_content.into_owned()
}
async fn generate_support_wits(
component_type: ComponentType,
output_directory: PathBuf,
force: bool,
) -> Result<Vec<GeneratedPathStatus>, anyhow::Error> {
let mut results = Vec::new();
let files = match component_type {
ComponentType::Activity => {
vec![wit::WIT_OBELISK_LOG_PACKAGE]
}
ComponentType::Workflow => vec![
wit::WIT_OBELISK_TYPES_PACKAGE,
wit::WIT_OBELISK_WORKFLOW_PACKAGE,
wit::WIT_OBELISK_LOG_PACKAGE,
],
ComponentType::WebhookEndpoint => {
vec![
wit::WIT_OBELISK_TYPES_PACKAGE, wit::WIT_OBELISK_WEBHOOK_PACKAGE,
wit::WIT_OBELISK_LOG_PACKAGE,
]
}
ComponentType::ActivityStub | ComponentType::Cron => vec![],
};
for [folder, filename, contents] in files {
let output_directory = output_directory.join(folder);
let target_wit = output_directory.join(filename);
if let Ok(actual) = tokio::fs::read_to_string(&target_wit).await
&& actual == contents
{
results.push(GeneratedPathStatus {
path: target_wit,
status: "up_to_date",
});
} else {
tokio::fs::create_dir_all(&output_directory)
.await
.with_context(|| format!("cannot write {output_directory:?}"))?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.create_new(!force)
.open(&target_wit)
.await
.with_context(|| {
format!(
"cannot open {target_wit:?} for writing{}",
if !force { ", try using `--force`" } else { "" }
)
})?;
file.write_all(contents.as_bytes())
.await
.with_context(|| format!("cannot write to {target_wit:?}"))?;
results.push(GeneratedPathStatus {
path: target_wit,
status: "created_or_updated",
});
}
}
Ok(results)
}
async fn generate_wit_deps(
project_dirs: Option<ProjectDirs>,
base_dirs: Option<BaseDirs>,
deployment_toml: PathBuf,
output_directory: PathBuf,
force: bool,
skip_local: bool,
secret_registry: Arc<SecretRegistry>,
) -> Result<Vec<GeneratedPathStatus>, anyhow::Error> {
let deployment = filter_wit_deps_deployment(
load_deployment_validated(&deployment_toml).await?,
skip_local,
);
let mut server_config = ServerConfigToml::default();
server_config.webui.enabled = false;
let _guard = init::init(&server_config)?; let deployment = deployment
.resolve()
.await
.with_context(|| format!("cannot resolve {deployment_toml:?}"))?;
let (termination_sender, mut termination_watcher) = watch::channel(());
tokio::spawn(async move { termination_notifier(termination_sender).await });
let verify_params = VerifyParams {
dir_params: PrepareDirsParams {
clean_cache: false,
clean_codegen_cache: false,
},
runtime_config_availability: RuntimeConfigAvailability::AllowUnavailable, suppress_type_checking_errors: true, suppress_linking_errors: true, };
let config_holder = ConfigHolder::new(project_dirs, base_dirs, None)?;
let prepared_dirs = prepare_dirs(
&server_config,
&verify_params.dir_params,
&config_holder.path_prefixes,
&secret_registry,
)
.await?;
let engines = create_engines(&server_config, &prepared_dirs)?;
let server_verified = Box::pin(server_verify(server_config, engines, secret_registry)).await?;
let deployment_verified = deployment_verify_config(
&server_verified,
&prepared_dirs,
deployment,
None,
verify_params.clone(),
&mut termination_watcher,
)
.await?;
let compiled_and_linked = deployment_compile_link(
server_verified,
deployment_verified,
DeploymentId::generate(),
verify_params,
&mut termination_watcher,
)
.await?;
tokio::fs::create_dir_all(&output_directory)
.await
.with_context(|| format!("cannot create the output directory {output_directory:?}"))?;
let mut pkg_to_wit: HashMap<PkgFqn, String> = HashMap::new();
let mut synthesized_exports: Vec<PackageIfcFns> = Vec::new();
for component in compiled_and_linked.component_registry_ro.list(true) {
let Some(importable) = &component.workflow_or_activity_config else {
unreachable!("webhooks and crons are filtered out, found {component:?}");
};
match component.wit_origin {
WitOrigin::Synthesized => {
synthesized_exports.extend(importable.exports_hierarchy_ext.iter().cloned());
}
WitOrigin::Authored => {
let requested_pkgs: Vec<PkgFqn> = importable
.exports_hierarchy_ext
.iter()
.map(|ifc_fns| ifc_fns.ifc_fqn.pkg_fqn_name())
.collect::<hashbrown::HashSet<_>>()
.into_iter()
.collect();
crate::wit_printer::process_pkg_with_deps(
&component.wit,
&requested_pkgs,
&mut pkg_to_wit,
)
.with_context(|| {
format!(
"cannot extract authored WIT packages from {}",
component.component_id
)
})?;
}
WitOrigin::Wasm => {
let requested_pkgs: Vec<PkgFqn> = importable
.exports_hierarchy_ext
.iter()
.map(|ifc_fns| ifc_fns.ifc_fqn.pkg_fqn_name())
.collect::<hashbrown::HashSet<_>>()
.into_iter()
.collect();
crate::wit_printer::process_pkg_with_deps(
&component.wit,
&requested_pkgs,
&mut pkg_to_wit,
)
.with_context(|| {
format!(
"cannot extract WIT packages from {}",
component.component_id
)
})?;
}
}
}
if !synthesized_exports.is_empty() {
let synthesized_map = wit::build_wit_deps_map(&synthesized_exports)?;
for (pkg_fqn, content) in synthesized_map {
pkg_to_wit.entry(pkg_fqn).or_insert(content);
}
}
write_wit_deps(&pkg_to_wit, &output_directory, force).await
}
fn filter_wit_deps_deployment(
mut deployment: DeploymentTomlValidated,
skip_local: bool,
) -> DeploymentTomlValidated {
if skip_local {
deployment
.activities_wasm
.retain(|c| matches!(c.common.location, ComponentLocationToml::Oci(_)));
deployment.activities_stub.retain(|(c, _)| match c {
ActivityStubComponentConfigToml::File(f) => {
matches!(f.common.location, ComponentLocationToml::Oci(_))
}
ActivityStubComponentConfigToml::Inline(_) => true,
});
deployment.activities_external.retain(|(c, _)| match c {
ActivityExternalComponentConfigToml::File(f) => {
matches!(f.common.location, ComponentLocationToml::Oci(_))
}
ActivityExternalComponentConfigToml::Inline(_) => true,
});
deployment.activities_js.retain(|(c, _)| {
c.content.is_none() && !matches!(c.location, Some(JsLocationToml::Path(_)))
});
deployment.activities_exec.retain(|(c, _)| {
c.content.is_none() && !matches!(c.location, Some(JsLocationToml::Path(_)))
});
deployment
.workflows_wasm
.retain(|c| matches!(c.common.location, ComponentLocationToml::Oci(_)));
deployment.workflows_js.retain(|(c, _)| {
c.content.is_none() && !matches!(c.location, Some(JsLocationToml::Path(_)))
});
}
deployment.webhooks_wasm.clear();
deployment.webhooks_js.clear();
deployment.crons.clear();
let remaining_names: HashSet<String> = deployment
.activities_wasm
.iter()
.map(|c| c.common.name.to_string())
.chain(
deployment
.activities_stub
.iter()
.map(|(_, name)| name.to_string()),
)
.chain(
deployment
.activities_external
.iter()
.map(|(_, name)| name.to_string()),
)
.chain(
deployment
.activities_js
.iter()
.map(|(_, name)| name.to_string()),
)
.chain(
deployment
.activities_exec
.iter()
.map(|(_, name)| name.to_string()),
)
.chain(
deployment
.workflows_wasm
.iter()
.map(|c| c.common.name.to_string()),
)
.chain(
deployment
.workflows_js
.iter()
.map(|(_, name)| name.to_string()),
)
.collect();
deployment
.component_names_to_types
.retain(|name, _| remaining_names.contains(name));
deployment
}
async fn write_wit_deps(
pkg_to_wit: &HashMap<PkgFqn, String>,
output_directory: &std::path::Path,
force: bool,
) -> Result<Vec<GeneratedPathStatus>, anyhow::Error> {
let mut results = Vec::new();
for (pkg_fqn, content) in pkg_to_wit {
let pkg_file_name = pkg_fqn.as_file_name();
let directory = output_directory.join(&pkg_file_name);
tokio::fs::create_dir_all(&directory)
.await
.with_context(|| format!("cannot create directory {directory:?}"))?;
let target_wit = directory.join(format!("{pkg_file_name}.wit"));
let old_content = tokio::fs::read_to_string(&target_wit)
.await
.unwrap_or_default();
let old_content = old_content
.split_once('\n')
.map(|(_, rest)| rest)
.unwrap_or("");
if content != old_content {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.create_new(!force)
.open(&target_wit)
.await
.with_context(|| {
format!(
"cannot open {target_wit:?} for writing{}",
if !force { ", try using `--force`" } else { "" }
)
})?;
let content = format!("{OBELISK_WIT_HEADER} {PKG_VERSION}\n{content}");
file.write_all(content.as_bytes())
.await
.with_context(|| format!("cannot write to {target_wit:?}"))?;
results.push(GeneratedPathStatus {
path: target_wit,
status: "written",
});
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::{
add_token_hash, generate_cli_schema, generate_component_metadata_annotation_schema,
generate_db_schema, generate_deployment_schema, generate_openapi_schema,
generate_server_config_schema,
};
use std::path::PathBuf;
#[test]
#[ignore = "updates committed schema assets"]
fn update_toml_schemas() {
generate_server_config_schema(Some(PathBuf::from("assets/schemas/toml/server.json")))
.unwrap();
generate_deployment_schema(Some(PathBuf::from("assets/schemas/toml/deployment.json")))
.unwrap();
}
#[test]
#[ignore = "updates committed schema assets"]
fn update_openapi_schema() {
generate_openapi_schema(Some(PathBuf::from("assets/schemas/openapi.json"))).unwrap();
}
#[test]
#[ignore = "updates committed schema assets"]
fn update_db_schema() {
generate_db_schema(Some(PathBuf::from("assets/schemas/db.json"))).unwrap();
}
#[test]
#[ignore = "updates committed schema assets"]
fn update_cli_schema() {
generate_cli_schema(Some(PathBuf::from("assets/schemas/cli.json"))).unwrap();
}
#[test]
#[ignore = "updates committed schema assets"]
fn update_component_metadata_annotation_schema() {
generate_component_metadata_annotation_schema(Some(PathBuf::from(
"assets/schemas/oci-metadata-annotation.json",
)))
.unwrap();
}
#[test]
fn add_token_hash_should_create_and_append() {
let hash = crate::api::token_hash("some-token");
let contents = add_token_hash("[webui]\nenabled = false\n", &hash).unwrap();
insta::assert_snapshot!("created", contents);
let contents = add_token_hash(
&format!("api.token_hashes = [\"{hash}\"]\n"),
&crate::api::token_hash("other-token"),
)
.unwrap();
insta::assert_snapshot!("appended", contents);
add_token_hash("[api]\ntoken_hashes = true\n", &hash).unwrap_err();
}
}