feat: add distributed service scaffold commands#55
Conversation
📝 WalkthroughWalkthroughAdds a new top-level ChangesService Command Feature
Sequence DiagramsequenceDiagram
participant CLI as CLI
participant Harness as ManifestHarness
participant Cargo as Cargo(subprocess)
participant Distributed as LocalDistributedCrate
CLI->>Harness: prepare harness options (target, entrypoint, mode)
Harness->>Distributed: resolve local distributed crate path
Harness->>Cargo: write temp workspace + main.rs and run `cargo run --manifest-path`
Cargo->>Harness: stdout JSON manifest or SQL schema
Harness->>CLI: return parsed manifest JSON or written SQL output
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
c90f90f to
6d7ba4e
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/service/mod.rs`:
- Around line 547-567: ScaffoldNames::new must reject inputs that produce
invalid Rust identifiers: after computing package_name, crate_ident
(package_name.replace('-', "_")) and model_struct
(to_pascal_case(&package_name)), call the existing is_rust_ident helper to
validate both crate_ident and model_struct (and view_struct if you want) and
return an Err if any are invalid; update the function (ScaffoldNames::new) to
perform these checks before constructing Self and include a clear error message,
referencing the symbols package_name, crate_ident, model_struct, view_struct,
and the is_rust_ident helper so generated scaffolds won't produce identifiers
starting with digits or otherwise invalid Rust names.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a86c273d-0559-4f8f-8cf1-d000bdbc844c
📒 Files selected for processing (4)
README.mdsrc/commands/mod.rssrc/commands/service/mod.rssrc/main.rs
| fn new(input: &str) -> Result<Self, Box<dyn Error>> { | ||
| let package_name = to_kebab_case(input); | ||
| if package_name.is_empty() { | ||
| return Err("service name must contain at least one ASCII letter or digit".into()); | ||
| } | ||
| let crate_ident = package_name.replace('-', "_"); | ||
| let model_struct = to_pascal_case(&package_name); | ||
| let view_struct = format!("{model_struct}View"); | ||
| let command_name = format!("{crate_ident}.create"); | ||
| let created_event = format!("{crate_ident}.created"); | ||
| let table_name = format!("{crate_ident}_views"); | ||
|
|
||
| Ok(Self { | ||
| package_name, | ||
| crate_ident, | ||
| model_struct, | ||
| view_struct, | ||
| command_name, | ||
| created_event, | ||
| table_name, | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are Rust's identifier rules for crate and type names? Can they start with a digit?
💡 Result:
In Rust, identifiers—which are used for naming items such as types and crates—cannot start with a digit [1][2]. The syntactic rules for identifiers are defined as follows: 1. Valid Start: An identifier must start with either a Unicode XID_Start character or an underscore () [1][2]. 2. Subsequent Characters: Following the initial character, an identifier can consist of any number of Unicode XID_Continue characters [1][2]. 3. Crates: Crate names follow these general identifier rules but have an additional restriction: they must only contain Unicode alphanumeric characters or the underscore () [3][4]. 4. Types: While type names must follow the general identifier rules, the community-standard API guidelines recommend using UpperCamelCase for types [5][6]. Because identifiers must begin with XID_Start or an underscore, they cannot start with a digit [1][2]. This restriction helps maintain clear syntax, as digits at the start of a token are reserved for numeric literals (e.g., integer or floating-point numbers) [7]. If you need an identifier that incorporates a number, you must prefix it with an underscore (e.g., _1_example) [7].
Citations:
- 1: https://doc.rust-lang.org/stable/reference/identifiers.html
- 2: https://doc.rust-lang.org/reference/identifiers.html
- 3: https://dev-doc.rust-lang.org/stable/reference/crates-and-source-files.html
- 4: https://doc.rust-lang.org/nightly/reference/crates-and-source-files.html
- 5: https://rust-lang.github.io/api-guidelines/naming.html
- 6: https://rust-lang.github.io/rfcs/0430-finalizing-naming-conventions.html
- 7: https://stackoverflow.com/questions/69210771/why-cant-an-identifier-start-with-a-number
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the surrounding code where ScaffoldNames::new constructs crate_ident/model_struct
rg -n "struct ScaffoldNames|fn new\(input: &str\)|to_kebab_case|to_pascal_case|crate_ident|model_struct" src/commands/service/mod.rs
echo "----"
# Print the relevant section (bounded) for context
sed -n '520,610p' src/commands/service/mod.rs
echo "----"
# Find where crate_ident/model_struct are interpolated into generated Rust code
rg -n "crate_ident|model_struct|view_struct|command_name|created_event|table_name" src/commands/service/mod.rs
echo "----"
# Search templates / codegen modules for these identifiers being emitted
rg -n "package_name|crate_ident|model_struct" src | head -n 200Repository: hops-ops/hops-cli
Length of output: 12439
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Print the to_kebab_case and to_pascal_case implementations and nearby tests
sed -n '900,1040p' src/commands/service/mod.rs
# Search for any existing helper that validates Rust identifiers / paths
rg -n "is_rust_ident|rust_ident|validate_rust_ident|identifier|Rust identifier" src/commands/service/mod.rs src | head -n 200Repository: hops-ops/hops-cli
Length of output: 3862
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '840,920p' src/commands/service/mod.rsRepository: hops-ops/hops-cli
Length of output: 2448
Reject service names that produce invalid Rust identifiers in the generated scaffold
ScaffoldNames::new currently turns "3d-checkout" into crate_ident = "3d_checkout" and model_struct = "3dCheckout", and those values are emitted as Rust identifiers (e.g., use crate::models::{model_struct};, pub struct {model_struct} { ... }, and {crate_ident}::service::in_memory()). Rust identifiers can’t start with a digit, so these scaffolds won’t compile. The file already has an is_rust_ident helper (used for validate_rust_path), but ScaffoldNames::new doesn’t use it.
Suggested fix
let crate_ident = package_name.replace('-', "_");
let model_struct = to_pascal_case(&package_name);
+ if !is_rust_ident(&crate_ident) || !is_rust_ident(&model_struct) {
+ return Err("service name must produce valid Rust identifiers".into());
+ }
let view_struct = format!("{model_struct}View");
let command_name = format!("{crate_ident}.create");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn new(input: &str) -> Result<Self, Box<dyn Error>> { | |
| let package_name = to_kebab_case(input); | |
| if package_name.is_empty() { | |
| return Err("service name must contain at least one ASCII letter or digit".into()); | |
| } | |
| let crate_ident = package_name.replace('-', "_"); | |
| let model_struct = to_pascal_case(&package_name); | |
| let view_struct = format!("{model_struct}View"); | |
| let command_name = format!("{crate_ident}.create"); | |
| let created_event = format!("{crate_ident}.created"); | |
| let table_name = format!("{crate_ident}_views"); | |
| Ok(Self { | |
| package_name, | |
| crate_ident, | |
| model_struct, | |
| view_struct, | |
| command_name, | |
| created_event, | |
| table_name, | |
| }) | |
| fn new(input: &str) -> Result<Self, Box<dyn Error>> { | |
| let package_name = to_kebab_case(input); | |
| if package_name.is_empty() { | |
| return Err("service name must contain at least one ASCII letter or digit".into()); | |
| } | |
| let crate_ident = package_name.replace('-', "_"); | |
| let model_struct = to_pascal_case(&package_name); | |
| if !is_rust_ident(&crate_ident) || !is_rust_ident(&model_struct) { | |
| return Err("service name must produce valid Rust identifiers".into()); | |
| } | |
| let view_struct = format!("{model_struct}View"); | |
| let command_name = format!("{crate_ident}.create"); | |
| let created_event = format!("{crate_ident}.created"); | |
| let table_name = format!("{crate_ident}_views"); | |
| Ok(Self { | |
| package_name, | |
| crate_ident, | |
| model_struct, | |
| view_struct, | |
| command_name, | |
| created_event, | |
| table_name, | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/service/mod.rs` around lines 547 - 567, ScaffoldNames::new must
reject inputs that produce invalid Rust identifiers: after computing
package_name, crate_ident (package_name.replace('-', "_")) and model_struct
(to_pascal_case(&package_name)), call the existing is_rust_ident helper to
validate both crate_ident and model_struct (and view_struct if you want) and
return an Err if any are invalid; update the function (ScaffoldNames::new) to
perform these checks before constructing Self and include a clear error message,
referencing the symbols package_name, crate_ident, model_struct, view_struct,
and the is_rust_ident helper so generated scaffolds won't produce identifiers
starting with digits or otherwise invalid Rust names.
5a55335 to
de56772
Compare
Implements [[tasks/hops-service-create-microsvc-scaffold]]. Updates [[customize-hops-service-scaffold-and-schema-output]]. Updates [[gitops-knative-service-scaffold]]. Updates [[replace-model-booleans-with-repeatable-model]]. Updates [[add-service-bus-flag]]. Updates [[make-service-read-models-opt-in]]. Updates [[rename-service-create-to-scaffold]]. Updates [[add-service-scaffold-github-workflows]].
de56772 to
8bdd30d
Compare
Summary
hops service scaffoldfor Distributed microsvc crates with handler/service/manifest modules;service createremains a hidden compatibility alias--model <name>for aggregate generation; no--modelflags generate stateless handlers and nosrc/modelsmodulesrc/models/todo.rs, withsrc/models/mod.rsas the index/re-export plus sharedCommandInput--read-models; default scaffolds do not generatesrc/read_models, do not register tables, andservice schemaemits no read-model SQL--commandand repeatable--event--transport knativeplus--knative; generated Knative services use Distributed's CloudEvents ingress and manifest transportknative--transportfor ingress shape (http/knative) and add--bus rabbitmq|kafka|psql|natsfor bus backend selection--gitopsto scaffold a Helm deploy chart under.gitops/deploy--gitops-promote argo|fluxto scaffold.gitops/promotewith an Argo CDApplicationor FluxGitRepository/HelmReleasepointed at.gitops/deploy--github <owner/repo>to configure release automation and create the target GitHub repo viagh repo create <owner/repo> --privatewhen it does not already exist--githubgenerates.github/workflows/version.yamlusingunbounded-tech/workflow-vnext-tagand.github/workflows/release.yamlusingunbounded-tech/workflow-simple-release--github-preview <owner/repo>to generate a preview promotion workflow usingunbounded-tech/workflows-gitopsand a.gitops/preview/helmpromotion chart--github-promote <owner/repo>to generate a permanent environment promotion workflow usingunbounded-tech/workflows-gitopsand a.gitops/promote/helmpromotion chartghcr.io/<owner>/<repo>when--githubis providedtodo->todo-commandsandtodo-eventstodo.create-> trigger ontodo-commandscheckout-saga.started-> trigger oncheckout-saga-eventsgithub-projects.issue.created-> trigger ongithub-projects-events--busis provided, generated GitOps values includebus.kindand deploy resources exposeHOPS_BUS--storageas a visible alias for--store, and--outputas a visible alias forservice schema --out[workspace]tableTests
cargo checkcargo test commands::servicecargo testrustfmt --check src/commands/service/mod.rsgit diff --checkhops service --helplistsscaffoldand notcreate;hops service scaffold --helpshows--model <MODEL>,--read-models,--bus <BUS>,--github <OWNER/REPO>,--github-preview <OWNER/REPO>, and--github-promote <OWNER/REPO>;--models,--no-models, and--no-read-modelsare absenthops service scaffold read-model-default --path .tmp/read-model-default --command todo.pingcargo checkpassed, nosrc/models, nosrc/read_models,service describereturnstables: [], and stdoutservice schemais emptyhops service scaffold read-model-explicit --path .tmp/read-model-explicit --model todo --read-models --command todo.createcargo checkpassed, generatedsrc/models/todo.rsandsrc/read_models/mod.rs,service describereturnstodo_views, andservice schemaemits onlytodo_viewshops service scaffold model-file-multi --path .tmp/model-file-multi --model todo --model somethingelse --command todo.create --command somethingelse.complete --read-modelscargo check,service describe, andservice schemapassed; generatedsrc/models/todo.rs,src/models/somethingelse.rs, andsrc/models/mod.rstodo_viewsandsomethingelse_viewshops service scaffold model-file-knative --path .tmp/model-file-knative --transport knative --gitops --gitops-promote flux --model todo --command todo.create --event github-projects.issue.created --bus kafkacargo checkpassed;.gitops/deploycontains Knative Service/Brokers/Triggers,.gitops/promotecontains FluxHelmRelease, and generated values/env includekafkagh:hops service scaffold test-domain --path .tmp/github-workflows --github hops-ops/test-domain --github-preview hops-ops/test-previews --github-promote hops-ops/test-staging --transport knative --bus nats --command test-domain.create --event github-projects.issue.createdgh repo view hops-ops/test-domain --json nameWithOwnerthengh repo create hops-ops/test-domain --privateversion.yaml,release.yaml,preview.yaml,promote.yaml,.gitops/preview/helm,.gitops/promote/helm, and.gitops/deploy;cargo checkpassedservice describereturns the Knative command/event manifest withtables: []; stdoutservice schemais empty--bus rabbitmqand--bus psqlboth compile and emitbus.kindplusHOPS_BUSservice describe, stdoutservice schema, andservice schema --output /tmp/todo-custom.sqlall succeededoutbox_messagesNotes
--busis carried in generated GitOps config/runtime env for now.DEPLOY_KEYfor vnext tagging andGH_ORG_ACTIONS_REPO_WRITE_PACKAGESfor GitOps environment repo writes.cargo fmt --checkstill reports unrelated formatting drift already present outside this service command change; this PR intentionally avoids that formatter churn.