use std::path::PathBuf;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClientProtocol {
#[default]
Rest,
Connect,
}
impl ClientProtocol {
pub(crate) fn subdir(self) -> &'static str {
match self {
ClientProtocol::Rest => "rest",
ClientProtocol::Connect => "connect",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClientProtocols {
pub rest: bool,
pub connect: bool,
}
impl Default for ClientProtocols {
fn default() -> Self {
Self {
rest: true,
connect: false,
}
}
}
impl ClientProtocols {
pub(crate) fn is_multi(self) -> bool {
self.rest && self.connect
}
pub(crate) fn selected(self) -> Vec<ClientProtocol> {
let mut v = Vec::new();
if self.rest {
v.push(ClientProtocol::Rest);
}
if self.connect {
v.push(ClientProtocol::Connect);
}
v
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Runtime {
#[default]
Prost,
Buffa,
}
#[derive(Debug, Clone)]
pub struct ModelsPath {
template: String,
}
impl ModelsPath {
pub fn new(template: &str) -> Result<Self> {
let test = template
.replace("{service}", "test")
.replace("{version}", "v1");
syn::parse_str::<syn::Path>(&test).map_err(|e| Error::InvalidModelsPathTemplate {
template: template.to_string(),
source: e,
})?;
Ok(Self::from_template(template))
}
pub(crate) fn from_template(template: &str) -> Self {
Self {
template: template.to_string(),
}
}
pub fn try_resolve(&self, service: &str, version: &str) -> Result<syn::Path> {
let path = self.substitute(service, version);
syn::parse_str(&path).map_err(|source| Error::InvalidModelsPathTemplate {
template: path,
source,
})
}
pub(crate) fn root(&self) -> Option<&str> {
let (prefix, _) = self.template.split_once("{service}")?;
let root = prefix.trim_end_matches(':');
(!root.is_empty()).then_some(root)
}
pub fn validate_for(&self, service: &str) -> Result<()> {
self.try_resolve(service, "v1").map(|_| ())
}
pub fn resolve(&self, service: &str, version: &str) -> syn::Path {
let path = self.substitute(service, version);
syn::parse_str(&path)
.unwrap_or_else(|e| panic!("Invalid models path `{path}` after substitution: {e}"))
}
fn substitute(&self, service: &str, version: &str) -> String {
self.template
.replace("{service}", service)
.replace("{version}", version)
}
}
#[derive(Debug, Clone)]
pub struct BindingsConfig {
pub aggregate_client_name: String,
pub client_crate_name: String,
pub py_error_type: String,
pub py_result_type: String,
pub napi_error_ext_trait: String,
pub ts_error_base_class: String,
pub ts_error_code_prefix: String,
pub python: Option<PythonOptions>,
}
#[derive(Debug, Clone)]
pub struct PythonOptions {
pub package: PyPackageConfig,
pub error_base_class: String,
pub module: String,
pub async_methods: bool,
pub keyword_only: bool,
pub public_modules: bool,
pub user_agent: Option<String>,
pub exports: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PyPackageConfig {
pub dir: PathBuf,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct CodeGenConfig {
pub context_type_path: String,
pub result_type_path: String,
pub models_path_template: String,
pub models_path_crate_template: String,
pub output: CodeGenOutput,
pub generate_resource_enum: bool,
pub generate_store_integration: bool,
pub error_type_path: Option<String>,
pub generate_object_conversions: bool,
pub bindings: Option<BindingsConfig>,
pub resource_store_crate_name: String,
pub runtime: Runtime,
pub transport_type_path: String,
pub dual_transport: bool,
pub client_protocols: ClientProtocols,
pub connect_client_path: Option<String>,
pub rest_client: RestClientOptions,
}
#[derive(Debug, Clone, Default)]
pub struct RestClientOptions {
pub json_int64_as_number: bool,
pub ignore_unknown_enum_values: bool,
pub error_module: Option<ErrorModuleConfig>,
pub form_encoded_methods: Vec<String>,
pub path_fields_in_body_methods: Vec<String>,
pub json_query_packages: Vec<String>,
pub user_agent: Option<String>,
pub resource_features: bool,
pub handler_impls: Option<ClientHandlerImpls>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientHandlerImpls {
pub crate_path: String,
pub feature: String,
pub skip: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ErrorModuleConfig {
pub code_field: String,
pub message_field: String,
pub details_field: Option<String>,
pub code_enum: Option<String>,
pub more_code_fields: Vec<String>,
pub more_message_fields: Vec<String>,
pub envelope_fields: Vec<String>,
}
impl Default for ErrorModuleConfig {
fn default() -> Self {
Self {
code_field: "error_code".into(),
message_field: "message".into(),
details_field: Some("details".into()),
code_enum: None,
more_code_fields: Vec::new(),
more_message_fields: Vec::new(),
envelope_fields: Vec::new(),
}
}
}
pub const DEFAULT_TRANSPORT_TYPE_PATH: &str = "olai_http::CloudClient";
impl CodeGenConfig {
pub fn uses_default_transport(&self) -> bool {
self.transport_type_path == DEFAULT_TRANSPORT_TYPE_PATH
}
pub fn uses_reqwest_transport(&self) -> bool {
!self.dual_transport() && self.transport_type_path == super::REQWEST_TRANSPORT_TYPE_PATH
}
pub fn builds_for_wasm(&self) -> bool {
self.dual_transport() || self.uses_reqwest_transport()
}
pub fn dual_transport(&self) -> bool {
self.output.wasm.is_some() || self.dual_transport
}
pub fn json_int64_module_path(&self) -> Option<String> {
if !self.rest_client.json_int64_as_number {
return None;
}
let models_path = ModelsPath::from_template(&self.models_path_crate_template);
Some(format!("{}::json_int64", models_path.root()?))
}
pub fn json_value_module_path(&self) -> Option<String> {
if !self.rest_client.json_int64_as_number {
return None;
}
let models_path = ModelsPath::from_template(&self.models_path_crate_template);
Some(format!("{}::json_value", models_path.root()?))
}
pub(crate) fn client_result_path(&self) -> syn::Path {
let path = match self.rest_client.error_module {
Some(_) => format!("{}::Result", super::error_module::CLIENT_ERROR_MODULE),
None => self.result_type_path.clone(),
};
syn::parse_str(&path).expect("valid result_type_path")
}
pub(crate) fn client_error_module(&self) -> syn::Path {
let path = match self.rest_client.error_module {
Some(_) => super::error_module::CLIENT_ERROR_MODULE,
None => "crate::error",
};
syn::parse_str(path).expect("valid module path")
}
pub(crate) fn python_options(&self) -> &PythonOptions {
self.bindings
.as_ref()
.and_then(|b| b.python.as_ref())
.expect("validated: the python output has `BindingsConfig::python`")
}
pub(crate) fn validate_options(&self) -> Result<()> {
let needs = |what: &str, requirement: &str| {
Err(Error::Build(format!("`{what}` needs {requirement}")))
};
if self.output.server.is_some() && self.output.common.is_none() {
return needs(
"CodeGenOutput::server",
"`CodeGenOutput::common` (the Axum extractors)",
);
}
if self.rest_client.user_agent.is_some() && !self.uses_reqwest_transport() {
return needs(
"RestClientOptions::user_agent",
"the generated `reqwest` transport",
);
}
if self.output.python.is_some() {
let Some(python) = self.bindings.as_ref().and_then(|b| b.python.as_ref()) else {
return needs("CodeGenOutput::python", "`BindingsConfig::python`");
};
for path in &python.exports {
syn::parse_str::<syn::Path>(path).map_err(|source| Error::InvalidRustPath {
path: path.clone(),
message: "PythonOptions::exports".to_string(),
source,
})?;
}
if python.user_agent.is_some() && !self.uses_reqwest_transport() {
return needs(
"PythonOptions::user_agent",
"the generated `reqwest` transport",
);
}
}
if self.rest_client.json_int64_as_number
&& ModelsPath::from_template(&self.models_path_crate_template)
.root()
.is_none()
{
return needs(
"RestClientOptions::json_int64_as_number",
"a `models_path_crate_template` of the form `<models root>::{service}::...`",
);
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
ModelsPath::new(&self.models_path_template)?;
ModelsPath::new(&self.models_path_crate_template)?;
let wants_bindings = self.output.python.is_some()
|| self.output.node.is_some()
|| self.output.node_ts.is_some()
|| self.output.wasm.is_some();
if wants_bindings && self.bindings.is_none() {
return Err(Error::MissingBindingsConfig);
}
if self.client_protocols.connect
&& self.output.client.is_some()
&& self.connect_client_path.is_none()
{
return Err(Error::MissingConnectClientPath);
}
if wants_bindings && !self.client_protocols.rest {
return Err(Error::BindingsRequireRest);
}
self.validate_options()
}
}
#[derive(Debug, Clone)]
pub struct CodeGenOutput {
pub common: Option<PathBuf>,
pub models: Option<PathBuf>,
pub models_subdir: String,
pub server: Option<PathBuf>,
pub client: Option<PathBuf>,
pub generate_resource_clients: bool,
pub python: Option<PathBuf>,
pub node: Option<PathBuf>,
pub node_ts: Option<PathBuf>,
pub wasm: Option<PathBuf>,
}
impl CodeGenOutput {
pub fn models_subdir_path(&self) -> Option<PathBuf> {
self.models.as_ref().map(|m| m.join(&self.models_subdir))
}
}