#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
use std::fmt::Write as _;
use sha2::{Digest, Sha256};
pub mod contract;
mod emitter;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedFile {
pub path: String,
pub contents: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedPackage {
pub venture: String,
pub package_name: String,
pub composition_hash: String,
pub files: Vec<GeneratedFile>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GenerateError {
UnsupportedSurfaceApi {
found: u32,
expected: u32,
},
NoTablesModule {
modules: Vec<String>,
},
UnknownAction {
action: String,
},
UnreadableAction {
action: String,
why: String,
},
UnsupportedTableName {
table: String,
},
}
impl std::fmt::Display for GenerateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GenerateError::UnsupportedSurfaceApi { found, expected } => write!(
f,
"the surface document speaks contract {found}, and this generator reads \
contract {expected}; generate the client with a generator built for it"
),
GenerateError::NoTablesModule { modules } if modules.is_empty() => write!(
f,
"the surface document declares no modules at all; a venture that serves \
declared tables publishes them as the `tables` module — compose the \
tables module into the venture and regenerate"
),
GenerateError::NoTablesModule { modules } => write!(
f,
"the surface document declares no `tables` module (it names [{}]); a \
client can only be generated from the tables the venture declares — \
compose the tables module into the venture and regenerate",
modules.join(", ")
),
GenerateError::UnknownAction { action } => write!(
f,
"the tables module publishes the action `{action}`, which is not one of \
the five verbs the generator reads (`list-`, `read-`, `create-`, \
`replace-`, `delete-`, each followed by the table name); regenerate \
with a generator that speaks this contract"
),
GenerateError::UnreadableAction { action, why } => write!(
f,
"the tables module publishes the action `{action}`, and {why}; the \
contract has drifted from its own vocabulary — regenerate with a \
generator that speaks it",
),
GenerateError::UnsupportedTableName { table } => write!(
f,
"the tables module publishes the table `{table}`, whose name is not a \
safe identifier — a table name must be lowercase `[a-z][a-z0-9_]*`, no \
double or trailing underscore — and it is interpolated into the \
generated TypeScript, so this generator refuses anything it cannot \
spell rather than renaming it; rename the table in the venture's \
declaration and regenerate",
),
}
}
}
impl std::error::Error for GenerateError {}
pub fn generate(
document: &cratefield_core::SurfaceDocument,
) -> Result<GeneratedPackage, GenerateError> {
let parsed = contract::extract(document)?;
let files = emitter::emit(&parsed);
let composition_hash = composition_hash_of(&files);
Ok(GeneratedPackage {
venture: parsed.venture,
package_name: emitter::PACKAGE_NAME.to_owned(),
composition_hash,
files,
})
}
#[must_use]
pub fn composition_hash_of(files: &[GeneratedFile]) -> String {
let mut hasher = Sha256::new();
for file in files {
hasher.update(file.path.as_bytes());
hasher.update([0]);
hasher.update(file.contents.as_bytes());
hasher.update([0]);
}
let digest = hasher.finalize();
digest.iter().fold(String::new(), |mut acc, byte| {
let _ = write!(acc, "{byte:02x}");
acc
})
}