Skip to content
Closed
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
119 changes: 119 additions & 0 deletions src/v3/compiler/src/omni_shape_b_openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,101 @@ pub fn project_markdown_documentation(dag: &Dag) -> Result<String, ProjectOpenAp
Ok(out)
}

pub fn project_rust_backend_service(dag: &Dag) -> Result<String, ProjectOpenApiError> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING: This expands hand-written Rust under src/v3, but the changed text only names Brief #2219 and does not provide the exact P5 receipt required for new hand Rust.

let routes = extract_rest_routes(dag)?;
let mut out = String::from(
r#"const ROUTES: &[(&str, &str)] = &[
"#,
);
for route in &routes {
out.push_str(" (");
out.push_str(&rust_string_literal(&route.method));
out.push_str(", ");
out.push_str(&rust_string_literal(&route.path));
out.push_str("),\n");
}
out.push_str(
r#"];

fn main() {
let mut args = std::env::args().skip(1);
let Some(method) = args.next() else {
print_routes();
return;
};
let Some(path) = args.next() else {
print_routes();
return;
};

for (route_method, route_template) in ROUTES {
if method.eq_ignore_ascii_case(route_method) && path_matches(route_template, &path) {
println!("200 {} {}", route_method, route_template);
return;
}
}
println!("404 {} {}", method.to_ascii_uppercase(), path);
}

fn print_routes() {
for (method, route_template) in ROUTES {
println!("{} {}", method, route_template);
}
}

fn path_matches(template: &str, candidate: &str) -> bool {
let template = template.as_bytes();
let candidate = candidate.as_bytes();
let mut t = 0;
let mut c = 0;
while t < template.len() {
if template[t] == b'{' {
let Some(close_offset) = template[t + 1..].iter().position(|byte| *byte == b'}') else {
return false;
};
let close = t + 1 + close_offset;
let after_param = close + 1;
if after_param == template.len() {
return c < candidate.len();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING: The terminal ParamToken branch accepts any non-empty suffix, so /users/{id} also matches /users/42/extra, breaking THESIS omni-emission coherence between the OpenAPI path and runnable backend.

}
let next_param = template[after_param..]
.iter()
.position(|byte| *byte == b'{')
.map(|offset| after_param + offset)
.unwrap_or(template.len());
let literal = &template[after_param..next_param];
let Some(next_match) = find_subslice(&candidate[c..], literal) else {
return false;
};
if next_match == 0 {
return false;
}
c += next_match;
t = after_param;
} else {
if candidate.get(c) != Some(&template[t]) {
return false;
}
t += 1;
c += 1;
}
}
c == candidate.len()
}

fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() {
return Some(0);
}
haystack
.windows(needle.len())
.position(|window| window == needle)
}
"#,
);
Ok(out)
}

fn append_path_parameter_yaml(out: &mut String, parameter: &str) {
out.push_str(" - name: ");
out.push_str(&yaml_double_quoted(parameter));
Expand Down Expand Up @@ -468,6 +563,25 @@ fn yaml_plain_operation_id(method: &str, path: &str) -> String {
format!("{}_{}", method.to_ascii_lowercase(), suffix)
}

fn rust_string_literal(value: &str) -> String {
let mut literal = String::from("\"");
for ch in value.chars() {
match ch {
'\\' => literal.push_str("\\\\"),
'"' => literal.push_str("\\\""),
'\n' => literal.push_str("\\n"),
'\r' => literal.push_str("\\r"),
'\t' => literal.push_str("\\t"),
other if other.is_control() => {
write!(&mut literal, "\\u{{{:X}}}", other as u32).expect("write to String");
}
other => literal.push(other),
}
}
literal.push('"');
literal
}

fn markdown_table_cell(value: &str) -> String {
value.replace('\\', "\\\\").replace('|', "\\|")
}
Expand Down Expand Up @@ -613,4 +727,9 @@ data service_operations: List<DemoOperation> = [
assert_eq!(markdown_code_span_table_cell("`x"), "`` `x ``");
assert_eq!(markdown_code_span_table_cell("x`"), "`` x` ``");
}

#[test]
fn rust_backend_projection_escapes_route_literals() {
assert_eq!(rust_string_literal("/a\"b\\c\n"), "\"/a\\\"b\\\\c\\n\"");
}
}
144 changes: 135 additions & 9 deletions src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,47 @@
//!
//! Brief #2219 receipt: one lowered endpoint-bearing fixture feeds the Shape B
//! OpenAPI 3.1 YAML projection, the Shape B Markdown drift-lock projection, and
//! the canonical route projection used as the interim backend exposure set. The
//! cross-target equality test is interim until a cross-target TestPredicate
//! variant exists.
//! a runnable Rust backend service projection.

use std::collections::{BTreeMap, BTreeSet};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};

use v3_compiler::compile_to_dag;
use v3_compiler::omni_shape_b_openapi::{
extract_rest_routes, project_markdown_documentation, project_openapi_yaml, RestRoute,
extract_rest_routes, project_markdown_documentation, project_openapi_yaml,
project_rust_backend_service, RestRoute,
};

static BACKEND_ROUNDTRIP_ID: AtomicUsize = AtomicUsize::new(0);

struct TmpDir(PathBuf);

impl TmpDir {
fn new() -> Self {
let id = BACKEND_ROUNDTRIP_ID.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"v3_omni_openapi_backend_{}_{}",
std::process::id(),
id
));
std::fs::create_dir_all(&path).expect("create tmp dir");
TmpDir(path)
}

fn path(&self) -> &Path {
&self.0
}
}

impl Drop for TmpDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}

const OMNI_SERVICE_FIXTURE: &str = r#"
module t.openapi_demo

Expand Down Expand Up @@ -304,6 +334,61 @@ fn yaml_scalar_value(value: &str) -> String {
value.to_string()
}

fn compile_backend_service(source: &str) -> (TmpDir, PathBuf) {
let tmp_dir = TmpDir::new();
let src_path = tmp_dir.path().join("backend.rs");
let bin_path = tmp_dir.path().join("backend_bin");
std::fs::File::create(&src_path)
.and_then(|mut f| f.write_all(source.as_bytes()))
.expect("write backend source");

let compile = Command::new("rustc")
.env_remove("RUSTC_BOOTSTRAP")
.arg("--edition=2021")
.arg(&src_path)
.arg("-o")
.arg(&bin_path)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.expect("invoke rustc");
assert!(
compile.success(),
"rustc failed on emitted backend source:\n{source}"
);
(tmp_dir, bin_path)
}

fn backend_stdout(bin_path: &Path, args: &[&str]) -> String {
let run = Command::new(bin_path)
.args(args)
.output()
.expect("run emitted backend");
assert!(
run.status.success(),
"emitted backend exited non-zero; stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
String::from_utf8_lossy(&run.stdout).trim().to_string()
}

fn backend_routes(source: &str) -> BTreeSet<RestRoute> {
let (_tmp_dir, bin_path) = compile_backend_service(source);
backend_stdout(&bin_path, &[])
.lines()
.map(|line| {
let (method, path) = line
.split_once(' ')
.expect("backend route listing is `METHOD PATH`");
RestRoute {
method: method.to_string(),
path: path.to_string(),
path_parameters: vec![],
}
})
.collect()
}

#[test]
fn markdown_documentation_parser_round_trips_escaped_table_cells() {
let markdown = concat!(
Expand Down Expand Up @@ -343,6 +428,8 @@ fn omni_layers_share_one_node_tree() {

let _canonical_routes = extract_rest_routes(&dag).expect("canonical route projection extracts");
let _openapi = project_openapi_yaml(&dag).expect("Shape B OpenAPI projects from shared DAG");
let _backend =
project_rust_backend_service(&dag).expect("Rust backend projects from shared DAG");
let _markdown =
project_markdown_documentation(&dag).expect("Shape B Markdown projects from shared DAG");

Expand Down Expand Up @@ -374,19 +461,58 @@ fn shape_b_openapi_projection_produces_3_1_yaml_for_rest_operations() {
}

#[test]
fn openapi_routes_match_canonical_dag_routes_interim() {
fn openapi_routes_match_emitted_backend_routes() {
let dag = compile_omni_service_fixture();

// Interim until cross-target TestPredicate exists: both projections are
// extracted from the same compiled DAG and compared here in `tests/`.
// Rust target does not yet expose a structured route projection, so this
// shared DAG extraction is the canonical backend exposure set for Brief #1.
let canonical_routes = extract_rest_routes(&dag).expect("canonical route projection extracts");
let openapi_routes =
openapi_yaml_routes(&project_openapi_yaml(&dag).expect("OpenAPI YAML projects"));
let backend_routes =
backend_routes(&project_rust_backend_service(&dag).expect("Rust backend service projects"));
let backend_routes_without_parameters: BTreeSet<_> = canonical_routes
.iter()
.map(|route| RestRoute {
method: route.method.clone(),
path: route.path.clone(),
path_parameters: vec![],
})
.collect();

assert_eq!(canonical_routes, expected_routes());
assert_eq!(openapi_routes, canonical_routes);
assert_eq!(backend_routes, backend_routes_without_parameters);
}

#[test]
fn emitted_backend_service_runs_declared_routes() {
let dag = compile_omni_service_fixture();
let source = project_rust_backend_service(&dag).expect("Rust backend service projects");
let (_tmp_dir, bin_path) = compile_backend_service(&source);

assert_eq!(
backend_stdout(&bin_path, &["GET", "/users"]),
"200 GET /users"
);
assert_eq!(
backend_stdout(&bin_path, &["POST", "/users"]),
"200 POST /users"
);
assert_eq!(
backend_stdout(&bin_path, &["GET", "/users/42"]),
"200 GET /users/{id}"
);
assert_eq!(
backend_stdout(&bin_path, &["GET", "/orgs/gunb/repos/gunbc"]),
"200 GET /orgs/{org}/repos/{repo}"
);
assert_eq!(
backend_stdout(&bin_path, &["POST", "/secrets/api_key:addVersion"]),
"200 POST /secrets/{secret_name}:addVersion"
);
assert_eq!(
backend_stdout(&bin_path, &["DELETE", "/users"]),
"404 DELETE /users"
);
}

#[test]
Expand Down
Loading