diff --git a/dsl/demos/todo_service.dag b/dsl/demos/todo_service.dag new file mode 100644 index 00000000000..5694eb809c4 --- /dev/null +++ b/dsl/demos/todo_service.dag @@ -0,0 +1,167 @@ +// demos/todo_service — minimal TODO domain + REST operation table for omni-emission demos. +// +// Exercises Shape B projections in `omni_shape_b_openapi.rs` (OpenAPI 3.1 YAML, +// Markdown drift-lock, SQL DDL route schema, runnable Rust backend) plus Shape A +// `emit_rust` on one shared `compile_to_dag` result (r3-program-plan §1.8 #25–#28). +// +// Domain carriers are illustrated with small `data` inhabitants (human-facing +// demo context). Omni route extraction still keys only on +// `List<{ endpoint: RestEndpointBinding }>` rows (`v3.std.services`). + +module demos.todo_service + +import std.types { GET, POST, PUT, PATCH, DELETE, Int, String, Bool } +import std.effects { LiteralToken, ParamToken } +import v3.std.services { RestEndpointBinding } + +// Trivial root `let` so the module is not purely type-level (same cadence as +// `omni_emit_anchor` in `m1_5_omni_shape_b_openapi_test` inline fixtures). Unused by route +// extractors; keeps demo symmetry with other omni Shape B receipts. +let todo_service_emit_anchor: Int = 0 + +type User { + id: String + display_name: String +} + +type TodoItem { + id: String + list_id: String + title: String + completed: Bool +} + +type TodoList { + id: String + owner_id: String + title: String +} + +type TodoServiceOperation { + endpoint: RestEndpointBinding +} + +data todo_domain_example_user: User = { + id: "demo-user", + display_name: "Demo User" +} + +data todo_domain_example_item: TodoItem = { + id: "demo-todo", + list_id: "demo-list", + title: "Wire domain carriers to example rows", + completed: false +} + +data todo_domain_example_list: TodoList = { + id: "demo-list", + owner_id: "demo-user", + title: "Demo backlog" +} + +data todo_service_operations: List = [ + { + endpoint: { + method: GET, + path: { tokens: [LiteralToken { text: "users" }] } + } + }, + { + endpoint: { + method: POST, + path: { tokens: [LiteralToken { text: "users" }] } + } + }, + { + endpoint: { + method: GET, + path: { + tokens: [LiteralToken { text: "users/" }, ParamToken { name: "userId" }] + } + } + }, + { + endpoint: { + method: PUT, + path: { + tokens: [LiteralToken { text: "users/" }, ParamToken { name: "userId" }] + } + } + }, + { + endpoint: { + method: DELETE, + path: { + tokens: [LiteralToken { text: "users/" }, ParamToken { name: "userId" }] + } + } + }, + { + endpoint: { + method: GET, + path: { + tokens: [ + LiteralToken { text: "users/" }, + ParamToken { name: "userId" }, + LiteralToken { text: "/todos" } + ] + } + } + }, + { + endpoint: { + method: POST, + path: { + tokens: [ + LiteralToken { text: "users/" }, + ParamToken { name: "userId" }, + LiteralToken { text: "/todos" } + ] + } + } + }, + { + endpoint: { + method: GET, + path: { + tokens: [LiteralToken { text: "todos/" }, ParamToken { name: "todoId" }] + } + } + }, + { + endpoint: { + method: PATCH, + path: { + tokens: [LiteralToken { text: "todos/" }, ParamToken { name: "todoId" }] + } + } + }, + { + endpoint: { + method: DELETE, + path: { + tokens: [LiteralToken { text: "todos/" }, ParamToken { name: "todoId" }] + } + } + }, + { + endpoint: { + method: GET, + path: { + tokens: [LiteralToken { text: "lists/" }, ParamToken { name: "listId" }] + } + } + }, + { + endpoint: { + method: POST, + path: { + tokens: [ + LiteralToken { text: "lists/" }, + ParamToken { name: "listId" }, + LiteralToken { text: "/todos" } + ] + } + } + } +] diff --git a/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs b/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs index 230c7de7ee5..7cd818a0dcb 100644 --- a/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs +++ b/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs @@ -129,6 +129,9 @@ import std.types { Int } let omni_emit_anchor: Int = 0 "#; +const TODO_SERVICE_REPOSITORY_SOURCE: &str = + include_str!("../../../../../dsl/demos/todo_service.dag"); + fn compile_omni_service_fixture() -> v3_compiler::Dag { std::thread::Builder::new() .name("m1_5_openapi_fixture_compile".to_string()) @@ -161,6 +164,89 @@ fn compile_empty_omni_service_fixture() -> v3_compiler::Dag { .expect("larger-stack compile thread completes") } +fn compile_todo_service_repository_fixture() -> v3_compiler::Dag { + std::thread::Builder::new() + .name("m1_5_todo_service_repository_compile".to_string()) + .stack_size(32 * 1024 * 1024) + .spawn(|| { + compile_to_dag(TODO_SERVICE_REPOSITORY_SOURCE, "dsl/demos/todo_service.dag") + .expect("repository todo_service.dag demo compiles") + }) + .expect("spawn larger-stack compile thread") + .join() + .expect("larger-stack compile thread completes") +} + +fn compile_todo_service_repository_fixture_counted(count: &mut usize) -> v3_compiler::Dag { + *count += 1; + compile_todo_service_repository_fixture() +} + +fn expected_todo_service_routes() -> BTreeSet { + BTreeSet::from([ + RestRoute { + method: "DELETE".to_string(), + path: "/todos/{todoId}".to_string(), + path_parameters: vec!["todoId".to_string()], + }, + RestRoute { + method: "DELETE".to_string(), + path: "/users/{userId}".to_string(), + path_parameters: vec!["userId".to_string()], + }, + RestRoute { + method: "GET".to_string(), + path: "/lists/{listId}".to_string(), + path_parameters: vec!["listId".to_string()], + }, + RestRoute { + method: "GET".to_string(), + path: "/todos/{todoId}".to_string(), + path_parameters: vec!["todoId".to_string()], + }, + RestRoute { + method: "GET".to_string(), + path: "/users".to_string(), + path_parameters: vec![], + }, + RestRoute { + method: "GET".to_string(), + path: "/users/{userId}".to_string(), + path_parameters: vec!["userId".to_string()], + }, + RestRoute { + method: "GET".to_string(), + path: "/users/{userId}/todos".to_string(), + path_parameters: vec!["userId".to_string()], + }, + RestRoute { + method: "PATCH".to_string(), + path: "/todos/{todoId}".to_string(), + path_parameters: vec!["todoId".to_string()], + }, + RestRoute { + method: "POST".to_string(), + path: "/lists/{listId}/todos".to_string(), + path_parameters: vec!["listId".to_string()], + }, + RestRoute { + method: "POST".to_string(), + path: "/users".to_string(), + path_parameters: vec![], + }, + RestRoute { + method: "POST".to_string(), + path: "/users/{userId}/todos".to_string(), + path_parameters: vec!["userId".to_string()], + }, + RestRoute { + method: "PUT".to_string(), + path: "/users/{userId}".to_string(), + path_parameters: vec!["userId".to_string()], + }, + ]) +} + fn expected_routes() -> BTreeSet { BTreeSet::from([ RestRoute { @@ -700,3 +786,70 @@ fn openapi_projection_ignores_same_shape_non_service_endpoint_binding() { an OpenAPI route unless its field type is the canonical RestEndpointBinding." ); } + +/// Repository `dsl/demos/todo_service.dag` visceral demo: one `compile_to_dag` feeds the +/// four Shape B projections (OpenAPI YAML, Markdown, SQL DDL, Rust backend) plus Shape A +/// `emit_rust`, matching r3-program-plan §1.8 #25–#28 structural discipline. +#[test] +fn todo_service_repository_demo_omni_layers_share_one_node_tree() { + // `compile_count` mirrors `omni_layers_share_one_node_tree`: one shared `Dag` is already + // enforced structurally because every projection takes `&dag`. The counter still ratchets + // accidental double acquisition via `compile_*_counted` (e.g. a duplicated helper call + // during edits), matching the §1.8 #28 receipt style in this module. + let mut compile_count = 0usize; + let dag = compile_todo_service_repository_fixture_counted(&mut compile_count); + + let canonical = extract_rest_routes(&dag).expect("canonical route projection extracts"); + assert_eq!(canonical, expected_todo_service_routes()); + + let openapi = project_openapi_yaml(&dag).expect("Shape B OpenAPI projects from shared DAG"); + assert!(openapi.starts_with("openapi: 3.1.0\n")); + assert_eq!(openapi_yaml_routes(&openapi), canonical); + + let markdown = project_markdown_documentation(&dag).expect("Markdown projects from shared DAG"); + assert_eq!(markdown_documentation_routes(&markdown), canonical); + + let ddl = project_sql_ddl_schema(&dag).expect("SQL DDL projects from shared DAG"); + let ddl_routes = sql_ddl_routes(&ddl); + let canonical_without_parameters: BTreeSet<_> = canonical + .iter() + .map(|route| RestRoute { + method: route.method.clone(), + path: route.path.clone(), + path_parameters: vec![], + }) + .collect(); + assert_eq!(ddl_routes, canonical_without_parameters); + assert!(ddl.contains(" CHECK (method IN (")); + assert!(ddl.contains("'DELETE'")); + assert!(ddl.contains("'PATCH'")); + assert!(ddl.contains("'PUT'")); + + let backend_source = + project_rust_backend_service(&dag).expect("backend service projects from shared DAG"); + let (_tmp_dir, backend_bin) = compile_backend_service(&backend_source); + for route in &canonical { + let concrete_path = route + .path + .replace("{userId}", "42") + .replace("{todoId}", "99") + .replace("{listId}", "7"); + assert_eq!( + backend_probe(&backend_bin, &route.method, &concrete_path), + "200", + "generated backend accepts route {} {}", + route.method, + concrete_path + ); + } + assert_eq!(backend_probe(&backend_bin, "GET", "/missing"), "404"); + assert_eq!(backend_probe(&backend_bin, "DELETE", "/users"), "404"); + assert_eq!(backend_probe(&backend_bin, "GET", "/users/"), "404"); + + let _rust = emit_rust(&dag).expect("Shape A Rust emit consumes shared DAG"); + + assert_eq!( + compile_count, 1, + "Shape A + Shape B layers must consume the same compile_to_dag result for the repository demo" + ); +}