Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions crates/pcl/core/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use std::{
env,
fmt::Write as _,
fs,
path::Path,
};

fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set");
let client_path =
Path::new(&manifest_dir).join("../../dapp-api-client/src/generated/client.rs");
println!("cargo:rerun-if-changed={}", client_path.display());

let source = fs::read_to_string(&client_path).unwrap_or_else(|error| {
panic!(
"failed to read generated dapp API client at {}: {error}",
client_path.display()
)
});
let entries = operation_entries(&source);
assert!(
!entries.is_empty(),
"failed to derive operation paths from generated dapp API client at {}",
client_path.display()
);

let out_dir = env::var("OUT_DIR").expect("OUT_DIR must be set");
let out_path = Path::new(&out_dir).join("generated_operation_paths.rs");
fs::write(out_path, generated_table(&entries)).expect("failed to write operation path table");
}

fn operation_entries(source: &str) -> Vec<(String, String, String)> {
let mut entries = Vec::new();
let mut pending_route: Option<(String, String)> = None;

for line in source.lines() {
if let Some(route) = sends_line_route(line) {
pending_route = Some(route);
continue;
}

let Some(operation_id) = operation_function_name(line) else {
continue;
};
let Some((method, path)) = pending_route.take() else {
continue;
};
if method_variant(&method).is_some() {
entries.push((operation_id, method, path));
}
}

entries
}

fn sends_line_route(line: &str) -> Option<(String, String)> {
let (_, rest) = line.trim().split_once("Sends a `")?;
let (method, rest) = rest.split_once("` request to `")?;
let (path, _) = rest.split_once('`')?;
Some((method.to_string(), path.to_string()))
}

fn operation_function_name(line: &str) -> Option<String> {
let rest = line.trim_start().strip_prefix("pub async fn ")?;
let end = rest.find(['<', '('])?;
Some(rest[..end].to_string())
}

fn generated_table(entries: &[(String, String, String)]) -> String {
let mut output =
String::from("const GENERATED_OPERATION_PATHS: &[(&str, HttpMethod, &str)] = &[\n");
for (operation_id, method, path) in entries {
let variant = method_variant(method).expect("entries contain supported HTTP methods");
writeln!(
output,
" ({operation_id:?}, HttpMethod::{variant}, {path:?}),"
)
.expect("writing generated operation table cannot fail");
}
output.push_str("];\n");
output
}

fn method_variant(method: &str) -> Option<&'static str> {
match method {
"GET" => Some("Get"),
"POST" => Some("Post"),
"PUT" => Some("Put"),
"PATCH" => Some("Patch"),
"DELETE" => Some("Delete"),
_ => None,
}
}
108 changes: 16 additions & 92 deletions crates/pcl/core/src/api/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,8 @@ use super::{
ApiCommandError,
HttpMethod,
};
use serde_json::Value;
use std::{
collections::HashMap,
sync::LazyLock,
};

static OPERATION_PATHS: LazyLock<HashMap<String, (HttpMethod, String)>> = LazyLock::new(|| {
let spec: Value = serde_json::from_str(include_str!(
"../../../../dapp-api-client/openapi/spec.json"
))
.expect("cached dapp OpenAPI spec must parse");
let mut operations = HashMap::new();
let paths = spec
.get("paths")
.and_then(Value::as_object)
.expect("cached dapp OpenAPI spec must contain paths");

for (path, path_item) in paths {
let Some(methods) = path_item.as_object() else {
continue;
};
for (method, operation) in methods {
let Some(method) = method_from_openapi_key(method) else {
continue;
};
let operation_id = operation
.get("operationId")
.and_then(Value::as_str)
.map_or_else(
|| generated_operation_id(method.openapi_key(), path),
ToString::to_string,
);
operations.insert(operation_id, (method, path.clone()));
}
}

operations
});
include!(concat!(env!("OUT_DIR"), "/generated_operation_paths.rs"));

#[derive(Clone, Debug)]
pub(in crate::api) struct WorkflowOperation {
Expand Down Expand Up @@ -67,15 +31,20 @@ impl WorkflowOperation {
}

pub(in crate::api) fn path(&self) -> Result<String, ApiCommandError> {
let (method, template) = OPERATION_PATHS.get(self.operation_id).ok_or_else(|| {
ApiCommandError::InvalidWorkflow {
message: format!(
"Generated OpenAPI operation `{}` was not found",
self.operation_id
),
}
})?;
if *method != self.method {
let (method, template) = GENERATED_OPERATION_PATHS
.iter()
.find_map(|(operation_id, method, template)| {
(*operation_id == self.operation_id).then_some((*method, *template))
})
.ok_or_else(|| {
ApiCommandError::InvalidWorkflow {
message: format!(
"Generated OpenAPI operation `{}` was not found",
self.operation_id
),
}
})?;
if method != self.method {
return Err(ApiCommandError::InvalidWorkflow {
message: format!(
"Generated OpenAPI operation `{}` uses method {}, not {}",
Expand All @@ -86,7 +55,7 @@ impl WorkflowOperation {
});
}

let mut path = template.clone();
let mut path = template.to_string();
for (name, value) in &self.path_params {
let encoded = encode_path_segment(value);
path = path.replace(&format!("{{{name}}}"), &encoded);
Expand All @@ -103,51 +72,6 @@ impl WorkflowOperation {
}
}

fn method_from_openapi_key(method: &str) -> Option<HttpMethod> {
match method {
"get" => Some(HttpMethod::Get),
"post" => Some(HttpMethod::Post),
"put" => Some(HttpMethod::Put),
"patch" => Some(HttpMethod::Patch),
"delete" => Some(HttpMethod::Delete),
_ => None,
}
}

fn generated_operation_id(method: &str, path: &str) -> String {
let path_parts = path
.split('/')
.filter(|segment| !segment.is_empty())
.map(generated_operation_segment)
.collect::<Vec<_>>()
.join("_");
format!("{method}_{path_parts}")
}

fn generated_operation_segment(segment: &str) -> String {
let segment = segment.trim_matches(|ch| ch == '{' || ch == '}');
let mut output = String::new();
let mut previous_was_lower_or_digit = false;

for ch in segment.chars() {
if ch.is_ascii_uppercase() {
if previous_was_lower_or_digit {
output.push('_');
}
output.push(ch.to_ascii_lowercase());
previous_was_lower_or_digit = false;
} else if ch.is_ascii_alphanumeric() {
output.push(ch.to_ascii_lowercase());
previous_was_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
} else if !output.ends_with('_') {
output.push('_');
previous_was_lower_or_digit = false;
}
}

output.trim_matches('_').to_string()
}

fn encode_path_segment(value: &str) -> String {
let mut encoded = String::with_capacity(value.len());

Expand Down
Loading