diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 854b284..9ee287f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,7 +83,7 @@ jobs: ! otool -L amber amber-lsp | grep -F 'openssl@1.1' - name: Smoke test generated Amber V2 web app - # Exercise the exact framework and asset releases emitted by the template. + # Exercise the exact published framework emitted by the template. run: scripts/smoke_generated_web.sh ./amber - name: Upload build artifacts diff --git a/.github/workflows/platform-compile.yml b/.github/workflows/platform-compile.yml index 347e471..1cb0916 100644 --- a/.github/workflows/platform-compile.yml +++ b/.github/workflows/platform-compile.yml @@ -27,7 +27,7 @@ jobs: run: crystal build src/amber_cli.cr --no-debug -o amber - name: Compile generated Amber V2 web app - # Exercise the exact framework and asset releases emitted by the template. + # Exercise the exact published framework emitted by the template. run: scripts/smoke_generated_web.sh ./amber windows-x86-64: @@ -58,5 +58,5 @@ jobs: - name: Compile generated Amber V2 web app shell: pwsh - # Exercise the exact framework and asset releases emitted by the template. + # Exercise the exact published framework emitted by the template. run: scripts/smoke_generated_web.ps1 ./amber.exe diff --git a/README.md b/README.md index e906b91..1985bec 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ [![GitHub release](https://img.shields.io/github/release/amberframework/amber_cli.svg)](https://github.com/amberframework/amber_cli/releases) [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://amberframework.github.io/amber_cli/) -Amber CLI is the standalone command-line companion for Amber V2. CLI `2.0.5` -creates the supported Amber `2.0.0-beta.4` ECR web application and includes +Amber CLI is the standalone command-line companion for Amber V2. CLI `2.0.6` +creates the supported Amber `2.0.0-beta.5` ECR web application and includes development, generator, database, and LSP tooling. Amber V2 is a beta. The release-gated path is a web application on Apple @@ -32,12 +32,12 @@ the `amber_cli` formula. ### Direct release archive -CLI `2.0.5` publishes `darwin-arm64`, `linux-x86_64`, and `linux-arm64` +CLI `2.0.6` publishes `darwin-arm64`, `linux-x86_64`, and `linux-arm64` archives. Windows x86-64 is compiled in CI but does not yet have a release archive. ```bash -version=v2.0.5 +version=v2.0.6 platform=darwin-arm64 asset="amber_cli-${platform}.tar.gz" @@ -70,7 +70,7 @@ or a file under `app/assets/` changes. Open . The web template is deliberately small: -- Amber from `amberframework/amber`, pinned to `2.0.0-beta.4` +- Amber from `amberframework/amber`, pinned to `2.0.0-beta.5` - ECR views (Slang and Kilt are not supported in Amber V2) - typed development, test, and production YAML - branded homepage, controller spec, and fingerprinted CSS, JavaScript, SVG, @@ -175,7 +175,7 @@ The release archive includes `amber-lsp`. From an Amber project: amber setup:lsp ``` -See the [LSP setup guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/lsp-setup.md). +See the [LSP setup guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/guides/lsp-setup.md). ## Contributing diff --git a/RELEASE_NOTES_V2.0.6.md b/RELEASE_NOTES_V2.0.6.md new file mode 100644 index 0000000..bcead40 --- /dev/null +++ b/RELEASE_NOTES_V2.0.6.md @@ -0,0 +1,63 @@ +# Amber CLI 2.0.6 + +Amber CLI 2.0.6 is the coordinated generator release for Amber +`2.0.0-beta.5`. Fresh web applications now use the framework's automatically +enforced schema contracts instead of constructing and validating a second +schema object inside each controller action. + +## Generate a complete web application + +```bash +amber new pet_tracker --type web +cd pet_tracker +shards install +amber assets check +amber generate scaffold Pet name:string:required species:string:required adopted:bool +amber database migrate +crystal spec +amber watch +``` + +The generated project pins canonical Amber `2.0.0-beta.5`, uses ECR, Grant, +SQLite, Micrate, local front-end assets, and a manifest-backed asset pipeline. + +## What changed + +- Resource scaffolds bind `PetSchema` automatically to `create` and `update` + and read typed request-local values with `validated_as(PetSchema)`. +- HTML form schemas declare URL-encoded input and return the generated ECR form + with visible field errors and HTTP 422 when a well-formed request is invalid. +- Malformed or unsupported form bodies preserve the framework's explicit 400 + or 415 status instead of being flattened into a generic validation error. +- Preview API generators bind the same executable schema contract for JSON + writes while retaining structured JSON failures. +- `amber generate schema` teaches the executable controller declaration as the + primary application path; direct construction remains available for isolated + schema unit tests. +- The Unix and Windows generated-app smoke harnesses can test an exact framework + repository and commit during coordinated development, while normal and + release CI now exercise the published beta emitted by the template. + +## Upgrade compatibility + +Amber's deprecated `params.validation` API remains functional. Existing Amber +applications can update the framework first, verify their current behavior, +and migrate validation one controller action at a time. CLI 2.0.6 changes new +generators; it does not force an existing application to regenerate its +controllers. + +## Release proof + +- 412 Amber CLI examples pass. +- Apple Silicon macOS and x86_64 Linux release-style binaries generate, build, + migrate, run, and exercise the complete web application. +- Linux ARM64 and Windows x86_64 compile and run the generated web application + in GitHub Actions. +- The Pet Tracker smoke proves invalid HTML form input returns one HTML + document with status 422 and a visible field error before proving persisted + create and PATCH update behavior. +- Fingerprinted CSS, JavaScript, SVG, and favicon responses retain integrity, + MIME, immutable caching, URL rewriting, and gzip checks. + +Windows remains a source-build compatibility path; this release publishes CLI +archives for Apple Silicon macOS, x86_64 Linux, and ARM64 Linux. diff --git a/docs/BETA_WEB_APP.md b/docs/BETA_WEB_APP.md index 9009374..c668d36 100644 --- a/docs/BETA_WEB_APP.md +++ b/docs/BETA_WEB_APP.md @@ -1,7 +1,7 @@ # Amber V2 Beta Web App -This guide is the consumer smoke test for Amber CLI `2.0.5` and Amber -`2.0.0-beta.4`. It is expected to pass on Apple Silicon macOS, x86_64 Linux, +This guide is the consumer smoke test for Amber CLI `2.0.6` and Amber +`2.0.0-beta.5`. It is expected to pass on Apple Silicon macOS, x86_64 Linux, and ARM64 Linux. Windows x86-64 compilation is checked in CI. ## 1. Verify the toolchain @@ -12,7 +12,7 @@ shards --version amber --version ``` -Crystal must be at least 1.20 and earlier than 2.0. Amber CLI must be 2.0.5 or +Crystal must be at least 1.20 and earlier than 2.0. Amber CLI must be 2.0.6 or newer. ## 2. Generate the web app @@ -30,7 +30,7 @@ grep -A2 'amber:' shard.yml grep -E '^(template|database|model):' .amber.yml ``` -The dependency must be `amberframework/amber` version `2.0.0-beta.4`; the +The dependency must be `amberframework/amber` version `2.0.0-beta.5`; the template must be `ecr`, the database must be `sqlite`, and the model layer must be `grant`. `shard.yml` must include Grant, Asset Pipeline, and `crystal-sqlite3`, but it must not include Gemma, Slang, PostgreSQL, or MySQL diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 04a25c7..b6ca695 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -6,14 +6,14 @@ Homebrew formula pins CLI archives and checksums. ## 1. Framework prerelease 1. Confirm `shard.yml`, `src/amber/version.cr`, and the changelog all say - `2.0.0-beta.4`. + `2.0.0-beta.5`. 2. Run framework specs and formatting on macOS and Linux. -3. Tag the reviewed `v2-dev` commit as `v2.0.0-beta.4`. +3. Tag the reviewed `v2-dev` commit as `v2.0.0-beta.5`. 4. Publish it as a GitHub prerelease with migration and support-matrix links. ## 2. CLI release -1. Confirm `shard.yml` and `AmberCLI::VERSION` both say `2.0.5`. +1. Confirm `shard.yml` and `AmberCLI::VERSION` both say `2.0.6`. 2. Generate a web app and verify it pins the framework prerelease. 3. Verify the generated app pins the reviewed Asset Pipeline revision and has no placeholder dependency revisions. @@ -27,7 +27,7 @@ Homebrew formula pins CLI archives and checksums. 6. On macOS, reject any binary linked to `openssl@1.1`. 7. Confirm the Windows x86-64 generated app builds in CI. This is a compatibility check, not a beta release gate. -8. Tag `v2.0.5`, publish the release, and wait for all archives and checksum +8. Tag `v2.0.6`, publish the release, and wait for all archives and checksum files to upload. ## 3. Homebrew diff --git a/scripts/check_beta_contract.sh b/scripts/check_beta_contract.sh index 20777c7..f6aa489 100755 --- a/scripts/check_beta_contract.sh +++ b/scripts/check_beta_contract.sh @@ -3,11 +3,11 @@ set -euo pipefail shard_version="$(awk '/^version:/ { print $2; exit }' shard.yml)" cli_version="$(sed -n 's/.*VERSION = "\([^"]*\)".*/\1/p' src/amber_cli.cr | head -1)" -test "$shard_version" = "2.0.5" +test "$shard_version" = "2.0.6" test "$cli_version" = "$shard_version" grep -F 'github: amberframework/amber' src/amber_cli/commands/new.cr -grep -F 'version: 2.0.0-beta.4' src/amber_cli/commands/new.cr +grep -F 'version: 2.0.0-beta.5' src/amber_cli/commands/new.cr grep -F 'template: ecr' src/amber_cli/commands/new.cr grep -F 'model: grant' src/amber_cli/commands/new.cr grep -F 'database: #{database}' src/amber_cli/commands/new.cr @@ -17,7 +17,7 @@ grep -F 'version: ~> 0.37.0' src/amber_cli/commands/new.cr grep -F 'github: amberframework/asset_pipeline' src/amber_cli/templates/app/shard.yml.ecr grep -F 'version: ~> 0.37.0' src/amber_cli/templates/app/shard.yml.ecr grep -F 'github: amberframework/amber' src/amber_cli/generators/native_app.cr -grep -F 'version: 2.0.0-beta.4' src/amber_cli/generators/native_app.cr +grep -F 'version: 2.0.0-beta.5' src/amber_cli/generators/native_app.cr grep -F 'version: ~> 0.37.0' src/amber_cli/generators/native_app.cr if grep -F 'branch:' src/amber_cli/generators/native_app.cr; then echo "native app template contains a mutable dependency branch" >&2 @@ -28,6 +28,24 @@ if grep -F 'shards install || true' src/amber_cli/generators/native_app.cr; then exit 1 fi grep -F 'github: amberframework/micrate' shard.yml +grep -F 'info " 2. Bind it in a controller: schema :create, #{class_name}Schema"' src/amber_cli/commands/generate.cr +grep -F 'content_type "#{content_type}"' src/amber_cli/commands/generate.cr +grep -F 'schema :create, #{class_name}Schema' src/amber_cli/commands/generate.cr +grep -F 'schema :update, #{class_name}Schema' src/amber_cli/commands/generate.cr +grep -F 'schema = validated_as(#{class_name}Schema)' src/amber_cli/commands/generate.cr +grep -F 'protected def handle_schema_validation_failure(' src/amber_cli/commands/generate.cr +if grep -F '# schema = #{class_name}Schema.new(data)' src/amber_cli/commands/generate.cr; then + echo "schema generator still teaches direct construction as the controller path" >&2 + exit 1 +fi + +grep -F 'scripts/smoke_generated_web.sh ./amber' .github/workflows/release.yml +if grep -R -F 'AMBER_CANDIDATE_FRAMEWORK' .github/workflows; then + echo "CI workflows must test the published framework emitted by the template" >&2 + exit 1 +fi +grep -F '[string]$FrameworkRepository = "amberframework/amber"' scripts/smoke_generated_web.ps1 +grep -F '" github: $FrameworkRepository"' scripts/smoke_generated_web.ps1 test -s src/amber_cli/templates/app/config/database.cr.ecr test -s src/amber_cli/templates/app/config/assets.cr.ecr grep -F 'Your new idea' src/amber_cli/commands/new.cr @@ -51,7 +69,7 @@ files=( README.md RELEASE_NOTES_V2.0.3.md RELEASE_NOTES_V2.0.4.md - RELEASE_NOTES_V2.0.5.md + RELEASE_NOTES_V2.0.6.md RELEASE_SETUP.md .github/ISSUE_TEMPLATE/release-checklist.md docs/*.md diff --git a/scripts/smoke_generated_web.ps1 b/scripts/smoke_generated_web.ps1 index 19aefe0..40f2192 100644 --- a/scripts/smoke_generated_web.ps1 +++ b/scripts/smoke_generated_web.ps1 @@ -2,7 +2,9 @@ param( [Parameter(Mandatory = $true, Position = 0)] [string]$Cli, [Parameter(Position = 1)] - [string]$FrameworkCommit = "" + [string]$FrameworkCommit = "", + [Parameter(Position = 2)] + [string]$FrameworkRepository = "amberframework/amber" ) $ErrorActionPreference = "Stop" @@ -37,6 +39,12 @@ $amberConfigPath = Join-Path $appPath ".amber.yml" $manifest = [System.IO.File]::ReadAllText($shardPath) $amberConfig = [System.IO.File]::ReadAllText($amberConfigPath) +if (-not $manifest.Contains("github: amberframework/amber")) { + throw "Generated shard.yml does not include Amber" +} +if (-not $manifest.Contains("version: 2.0.0-beta.5")) { + throw "Generated shard.yml does not pin the supported Amber beta" +} if (-not $manifest.Contains("github: crimson-knight/grant")) { throw "Generated shard.yml does not include Grant" } @@ -54,10 +62,18 @@ if (-not $amberConfig.Contains("database: sqlite") -or -not $amberConfig.Contain } if ($FrameworkCommit) { - $releasedFramework = " version: 2.0.0-beta.4" + $releasedRepository = " github: amberframework/amber" + $releasedFramework = " version: 2.0.0-beta.5" + if (-not $manifest.Contains($releasedRepository)) { + throw "Generated shard.yml does not contain the expected Amber repository" + } if (-not $manifest.Contains($releasedFramework)) { throw "Generated shard.yml does not contain the expected Amber beta pin" } + $manifest = $manifest.Replace( + $releasedRepository, + " github: $FrameworkRepository" + ) $manifest = $manifest.Replace( $releasedFramework, " commit: $FrameworkCommit" diff --git a/scripts/smoke_generated_web.sh b/scripts/smoke_generated_web.sh index 9f22dae..6ced71b 100755 --- a/scripts/smoke_generated_web.sh +++ b/scripts/smoke_generated_web.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -lt 1 || $# -gt 2 ]]; then - echo "usage: $0 /path/to/amber [framework-commit]" >&2 +if [[ $# -lt 1 || $# -gt 3 ]]; then + echo "usage: $0 /path/to/amber [framework-commit] [framework-repository]" >&2 exit 64 fi cli_path="$1" framework_commit="${2:-}" +framework_repository="${3:-amberframework/amber}" script_dir="$(cd "$(dirname "$0")" && pwd)" if [[ "$cli_path" != /* ]]; then cli_path="$(cd "$(dirname "$cli_path")" && pwd)/$(basename "$cli_path")" @@ -30,7 +31,7 @@ trap cleanup EXIT "$cli_path" new "$app_path" --type web --no-deps grep -F "github: amberframework/amber" "$app_path/shard.yml" -grep -F "version: 2.0.0-beta.4" "$app_path/shard.yml" +grep -F "version: 2.0.0-beta.5" "$app_path/shard.yml" grep -F "github: crimson-knight/grant" "$app_path/shard.yml" grep -F "github: amberframework/asset_pipeline" "$app_path/shard.yml" grep -F "version: ~> 0.37.0" "$app_path/shard.yml" @@ -50,6 +51,7 @@ if grep -Eiq 'gemma:|slang' "$app_path/shard.yml" "$app_path/.amber.yml"; then fi if [[ -n "$framework_commit" ]]; then + sed -i.bak -E "s# github: amberframework/amber# github: ${framework_repository}#" "$app_path/shard.yml" sed -i.bak -E "s/ version: 2\.0\.0-beta\.[0-9]+/ commit: ${framework_commit}/" "$app_path/shard.yml" rm -f "$app_path/shard.yml.bak" fi @@ -74,6 +76,15 @@ grep -F "class Pet < Grant::Base" src/models/pet.cr grep -F -- "-- +micrate Up" db/migrations/*_create_pets.sql grep -F -- "-- +micrate Down" db/migrations/*_create_pets.sql grep -F 'resources "/pets", PetController' config/routes.cr +grep -F 'content_type "application/x-www-form-urlencoded"' src/schemas/pet_schema.cr +grep -F 'schema :create, PetSchema' src/controllers/pet_controller.cr +grep -F 'schema :update, PetSchema' src/controllers/pet_controller.cr +grep -F 'validated_as(PetSchema)' src/controllers/pet_controller.cr +grep -F 'handle_schema_validation_failure' src/controllers/pet_controller.cr +if grep -F 'PetSchema.new(merge_request_data)' src/controllers/pet_controller.cr; then + echo "generated scaffold bypasses Amber's automatic controller contract" >&2 + exit 1 +fi grep -F 'render(partial: "_form.ecr")' src/views/pet/new.ecr grep -F 'hidden_field("_method", "PATCH")' src/views/pet/_form.ecr @@ -163,6 +174,24 @@ grep -F 'name="adopted"' "$new_page" | grep -F 'value="true"' csrf_token="$(sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' "$new_page" | head -1)" test -n "$csrf_token" +invalid_page="$smoke_root/invalid-pet.html" +invalid_headers="$smoke_root/invalid-pet-headers.txt" +invalid_status="$(curl --silent --output "$invalid_page" --dump-header "$invalid_headers" --write-out '%{http_code}' \ + --cookie "$cookie_jar" --request POST \ + --data-urlencode "_csrf=$csrf_token" \ + --data-urlencode "name=Missing Species" \ + http://127.0.0.1:3210/pets)" +test "$invalid_status" = "422" +grep -i -F "Content-Type: text/html" "$invalid_headers" +test "$(grep -c '' "$invalid_page")" -eq 1 +grep -F "Please correct the following" "$invalid_page" +grep -F "species:" "$invalid_page" + +curl --fail --silent --cookie "$cookie_jar" --cookie-jar "$cookie_jar" \ + http://127.0.0.1:3210/pets/new >"$new_page" +csrf_token="$(sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' "$new_page" | head -1)" +test -n "$csrf_token" + curl --fail --silent --output /dev/null --dump-header "$response_headers" \ --cookie "$cookie_jar" --request POST \ --data-urlencode "_csrf=$csrf_token" \ diff --git a/shard.yml b/shard.yml index 6fee04b..d6f323b 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: amber_cli -version: 2.0.5 +version: 2.0.6 authors: - crimson-knight diff --git a/spec/commands/generate_command_spec.cr b/spec/commands/generate_command_spec.cr index 25a6ade..6e6a72e 100644 --- a/spec/commands/generate_command_spec.cr +++ b/spec/commands/generate_command_spec.cr @@ -58,14 +58,62 @@ describe AmberCLI::Commands::GenerateCommand do File.read(migration).should contain("-- +micrate Down") File.read("config/routes.cr").should contain(%(resources "/pets", PetController)) - File.read("src/controllers/pet_controller.cr").should contain("schema.adopted") - File.read("src/controllers/pet_controller.cr").should_not contain("schema.adopted.not_nil!") + schema = File.read("src/schemas/pet_schema.cr") + schema.should contain(%(content_type "application/x-www-form-urlencoded")) + + controller = File.read("src/controllers/pet_controller.cr") + controller.should contain("schema :create, PetSchema") + controller.should contain("schema :update, PetSchema") + controller.should contain("validated_as(PetSchema)") + controller.should contain("schema.adopted") + controller.should_not contain("schema.adopted.not_nil!") + controller.should contain("handle_schema_validation_failure") + controller.should contain(%(context.content = render("new.ecr"))) + controller.should_not contain("PetSchema.new(merge_request_data)") File.read("src/views/pet/new.ecr").should contain(%(render(partial: "_form.ecr"))) File.read("src/views/pet/_form.ecr").should contain(%(hidden_field("_method", "PATCH"))) File.read("src/views/pet/_form.ecr").should contain(%(checkbox("adopted", checked: @pet.adopted? || false, value: "true"))) end end + it "generates schemas with the executable controller contract as the primary path" do + SpecHelper.within_temp_directory do + command = AmberCLI::Commands::GenerateCommand.new("generate") + command.parse_and_execute(["schema", "Post", "title:string:required"]) + + schema = File.read("src/schemas/post_schema.cr") + schema.should contain("schema :create, PostSchema") + schema.should contain("validated_as(PostSchema)") + schema.should_not contain("PostSchema.new(data)") + + spec = File.read("spec/schemas/post_schema_spec.cr") + spec.should contain("PostSchema.new(data)") + end + end + + it "generates API writes with automatically enforced JSON schemas" do + SpecHelper.within_temp_directory do + File.write(".amber.yml", "template: ecr\ndatabase: sqlite\nmodel: grant\n") + + command = AmberCLI::Commands::GenerateCommand.new("generate") + command.parse_and_execute([ + "api", + "Pet", + "name:string:required", + "adopted:bool", + ]) + + schema = File.read("src/schemas/pet_schema.cr") + schema.should contain(%(content_type "application/json")) + + controller = File.read("src/controllers/api/pet_controller.cr") + controller.should contain("schema :create, PetSchema") + controller.should contain("schema :update, PetSchema") + controller.should contain("validated_as(PetSchema)") + controller.should_not contain("PetSchema.new(merge_request_data)") + end + end + it "adds scaffold routes to Windows-style route files" do SpecHelper.within_temp_directory do File.write(".amber.yml", "template: ecr\ndatabase: sqlite\nmodel: grant\n") diff --git a/spec/commands/new_command_spec.cr b/spec/commands/new_command_spec.cr index fa3e908..d68c6ae 100644 --- a/spec/commands/new_command_spec.cr +++ b/spec/commands/new_command_spec.cr @@ -86,7 +86,7 @@ describe AmberCLI::Commands::NewCommand do shard = File.read(File.join(destination, "shard.yml")) shard.should contain("github: amberframework/amber") - shard.should contain("version: 2.0.0-beta.4") + shard.should contain("version: 2.0.0-beta.5") shard.should contain("grant:") shard.should contain("github: crimson-knight/grant") shard.should contain("asset_pipeline:") diff --git a/spec/generators/native_app_spec.cr b/spec/generators/native_app_spec.cr index ce0f1c8..36eb1a1 100644 --- a/spec/generators/native_app_spec.cr +++ b/spec/generators/native_app_spec.cr @@ -31,7 +31,7 @@ describe AmberCLI::Generators::NativeApp do # Must have amber (patterns only) shard_content.should contain("amber:") shard_content.should contain("github: amberframework/amber") - shard_content.should contain("version: 2.0.0-beta.4") + shard_content.should contain("version: 2.0.0-beta.5") # Must have the released asset_pipeline with cross-platform UI support shard_content.should contain("asset_pipeline:") diff --git a/src/amber_cli.cr b/src/amber_cli.cr index e78527a..b886e52 100644 --- a/src/amber_cli.cr +++ b/src/amber_cli.cr @@ -40,7 +40,7 @@ end Log.builder.bind "*", :info, backend module AmberCLI - VERSION = "2.0.5" + VERSION = "2.0.6" def self.run(args = ARGV) if args.empty? diff --git a/src/amber_cli/commands/generate.cr b/src/amber_cli/commands/generate.cr index 456ccf9..10de627 100644 --- a/src/amber_cli/commands/generate.cr +++ b/src/amber_cli/commands/generate.cr @@ -292,7 +292,7 @@ RETRIES # #{class_name}.new.enqueue(delay: 5.minutes) # #{class_name}.new.enqueue(queue: "critical") # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/background-jobs.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/guides/background-jobs.md class #{class_name} < Amber::Jobs::Job include JSON::Serializable @@ -390,7 +390,7 @@ METHOD # .subject("Welcome!") # .deliver # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/mailer.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/guides/mailer.md class #{class_name}Mailer < Amber::Mailer::Base def initialize(@user_name : String, @user_email : String) end @@ -455,8 +455,8 @@ SPEC puts "" info "Next steps:" info " 1. Customize field validations (min_length, max_length, format, etc.)" - info " 2. Use in controllers: schema = #{class_name}Schema.new(merge_request_data)" - info " 3. Check result: result = schema.validate" + info " 2. Bind it in a controller: schema :create, #{class_name}Schema" + info " 3. Read typed input: input = validated_as(#{class_name}Schema)" end private def schema_template @@ -484,17 +484,14 @@ SPEC <<-SCHEMA # Schema definition for validating #{class_name.underscore.gsub("_", " ")} data. # -# Usage: -# data = {"name" => JSON::Any.new("value")} -# schema = #{class_name}Schema.new(data) -# result = schema.validate -# if result.success? -# # Access validated fields: schema.name -# else -# # Handle errors: result.errors -# end +# Bind this contract above an action in its controller: +# schema :create, #{class_name}Schema +# +# Amber enforces it before the action. Read its request-local typed values with: +# input = validated_as(#{class_name}Schema) # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/schema-api.md +# Direct construction remains useful in this schema's isolated unit spec. +# See: https://amberframework.org/docs/v2/guides/schema-api/ class #{class_name}Schema < Amber::Schema::Definition #{field_definitions} end @@ -599,7 +596,7 @@ SPEC # Clients subscribe to this channel through a ClientSocket. # Messages sent to this channel are handled by `handle_message`. # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/websockets.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/guides/websockets.md class #{class_name}Channel < Amber::WebSockets::Channel # Called when a client subscribes to this channel. # Use this for authorization or sending initial state. @@ -630,7 +627,7 @@ CHANNEL # Configure in config/routes.cr: # websocket "/#{file_name}", #{class_name}Socket # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/websockets.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/guides/websockets.md struct #{class_name}Socket < Amber::WebSockets::ClientSocket channel "#{file_name}:*", #{class_name}Channel @@ -843,7 +840,7 @@ SPEC info "Run 'amber database migrate' before opening /#{plural_name}." end - private def generate_scaffold_schema + private def generate_scaffold_schema(content_type = "application/x-www-form-urlencoded") schema_path = "src/schemas/#{file_name}_schema.cr" field_definitions = schema_fields.map do |field_name, field_type, is_required| @@ -859,8 +856,10 @@ SPEC # # Used by #{controller_name} for request validation. # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/schema-api.md +# See: https://amberframework.org/docs/v2/guides/schema-api/ class #{class_name}Schema < Amber::Schema::Definition + content_type "#{content_type}" + #{field_definitions} end SCHEMA @@ -881,16 +880,19 @@ SCHEMA schema_field_assignments = fields.map do |field_name, _| suffix = field_required?(field_name) ? ".not_nil!" : "" - " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" + " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" end.join("\n") update_field_assignments = fields.map do |field_name, _| suffix = field_required?(field_name) ? ".not_nil!" : "" - " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" + " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" end.join("\n") <<-CONTROLLER class #{controller_name} < ApplicationController + schema :create, #{class_name}Schema + schema :update, #{class_name}Schema + @#{plural_variable_name} = [] of #{class_name} @#{variable_name} = #{class_name}.new @errors = [] of Amber::Schema::Error @@ -916,26 +918,16 @@ class #{controller_name} < ApplicationController end def create - # Schema-based parameter validation - schema = #{class_name}Schema.new(merge_request_data) - result = schema.validate - - if result.success? - #{variable_name} = #{class_name}.new + schema = validated_as(#{class_name}Schema) + #{variable_name} = #{class_name}.new #{schema_field_assignments} - if #{variable_name}.save - flash[:success] = "#{class_name} created successfully" - redirect_to "/#{plural_name}/\#{#{variable_name}.id}" - else - @#{variable_name} = #{variable_name} - flash[:danger] = "Could not create #{class_name}" - render("new.#{template_ext}") - end + if #{variable_name}.save + flash[:success] = "#{class_name} created successfully" + redirect_to "/#{plural_name}/\#{#{variable_name}.id}" else - @#{variable_name} = #{class_name}.new - @errors = result.errors - flash[:danger] = "Validation failed" + @#{variable_name} = #{variable_name} + flash[:danger] = "Could not create #{class_name}" render("new.#{template_ext}") end end @@ -952,24 +944,15 @@ class #{controller_name} < ApplicationController def update if #{variable_name} = #{class_name}.find(params[:id]) - schema = #{class_name}Schema.new(merge_request_data) - result = schema.validate - - if result.success? + schema = validated_as(#{class_name}Schema) #{update_field_assignments} - if #{variable_name}.save - flash[:success] = "#{class_name} updated successfully" - redirect_to "/#{plural_name}/\#{#{variable_name}.id}" - else - @#{variable_name} = #{variable_name} - flash[:danger] = "Could not update #{class_name}" - render("edit.#{template_ext}") - end + if #{variable_name}.save + flash[:success] = "#{class_name} updated successfully" + redirect_to "/#{plural_name}/\#{#{variable_name}.id}" else @#{variable_name} = #{variable_name} - @errors = result.errors - flash[:danger] = "Validation failed" + flash[:danger] = "Could not update #{class_name}" render("edit.#{template_ext}") end else @@ -987,6 +970,33 @@ class #{controller_name} < ApplicationController end redirect_to "/#{plural_name}" end + + protected def handle_schema_validation_failure( + action : Symbol, + result : Amber::Schema::LegacyResult, + ) : Nil + @errors = result.errors + error = result.errors.first? + response.status_code = error.is_a?(Amber::Schema::RequestParseError) ? error.http_status : 422 + response.content_type = "text/html" + flash[:danger] = "Validation failed" + + case action + when :create + @#{variable_name} = #{class_name}.new + context.content = render("new.#{template_ext}") + when :update + if #{variable_name} = #{class_name}.find(params[:id]) + @#{variable_name} = #{variable_name} + context.content = render("edit.#{template_ext}") + else + flash[:danger] = "#{class_name} not found" + redirect_to "/#{plural_name}" + end + else + super + end + end end CONTROLLER end @@ -1102,7 +1112,7 @@ SQL generate_model # Generate schema for API validation - generate_scaffold_schema + generate_scaffold_schema("application/json") # API controller (JSON only) api_dir = "src/controllers/api" @@ -1136,6 +1146,9 @@ SQL <<-CONTROLLER module Api class #{controller_name} < ApplicationController + schema :create, #{class_name}Schema + schema :update, #{class_name}Schema + def index #{plural_variable_name} = #{class_name}.all.to_a render json: #{plural_variable_name}.to_json @@ -1150,38 +1163,26 @@ module Api end def create - schema = #{class_name}Schema.new(merge_request_data) - result = schema.validate - - if result.success? - #{variable_name} = #{class_name}.new + schema = validated_as(#{class_name}Schema) + #{variable_name} = #{class_name}.new #{schema_field_assignments} - if #{variable_name}.save - render json: #{variable_name}.to_json, status: 201 - else - render json: {error: "Could not create #{class_name}"}.to_json, status: 422 - end + if #{variable_name}.save + render json: #{variable_name}.to_json, status: 201 else - render json: {errors: result.errors.map(&.to_h)}.to_json, status: 422 + render json: {error: "Could not create #{class_name}"}.to_json, status: 422 end end def update if #{variable_name} = #{class_name}.find(params[:id]) - schema = #{class_name}Schema.new(merge_request_data) - result = schema.validate - - if result.success? + schema = validated_as(#{class_name}Schema) #{update_field_assignments} - if #{variable_name}.save - render json: #{variable_name}.to_json - else - render json: {error: "Could not update #{class_name}"}.to_json, status: 422 - end + if #{variable_name}.save + render json: #{variable_name}.to_json else - render json: {errors: result.errors.map(&.to_h)}.to_json, status: 422 + render json: {error: "Could not update #{class_name}"}.to_json, status: 422 end else render json: {error: "#{class_name} not found"}.to_json, status: 404 diff --git a/src/amber_cli/commands/new.cr b/src/amber_cli/commands/new.cr index 5c168c7..0876509 100644 --- a/src/amber_cli/commands/new.cr +++ b/src/amber_cli/commands/new.cr @@ -284,7 +284,7 @@ targets: dependencies: amber: github: amberframework/amber - version: 2.0.0-beta.4 + version: 2.0.0-beta.5 grant: github: crimson-knight/grant commit: 2665a978b43ac608c68cde9243821f8f8f053372 @@ -316,7 +316,7 @@ AMBER readme_content = <<-README # #{name} -An ECR web application generated by Amber CLI for Amber `2.0.0-beta.4`. +An ECR web application generated by Amber CLI for Amber `2.0.0-beta.5`. ## Run it @@ -353,7 +353,7 @@ server database. Set `DATABASE_URL` to override the environment YAML URL. ## Production configuration Set at least `AMBER_SERVER_SECRET_KEY_BASE` and `DATABASE_URL`. See the -[Amber V2 beta guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/beta-installation.md). +[Amber V2 beta guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/beta-installation.md). README write_text(File.join(path, "README.md"), readme_content) diff --git a/src/amber_cli/commands/setup_lsp.cr b/src/amber_cli/commands/setup_lsp.cr index 55d6d30..94adcb1 100644 --- a/src/amber_cli/commands/setup_lsp.cr +++ b/src/amber_cli/commands/setup_lsp.cr @@ -215,7 +215,7 @@ module AmberCLI::Commands content = <<-YAML # Amber LSP Configuration - # See: https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/guides/lsp-setup.md + # See: https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/guides/lsp-setup.md # Override built-in rule settings # rules: diff --git a/src/amber_cli/documentation.cr b/src/amber_cli/documentation.cr index 4eb1775..fafc643 100644 --- a/src/amber_cli/documentation.cr +++ b/src/amber_cli/documentation.cr @@ -89,7 +89,7 @@ module AmberCLI::Documentation # - **.amber.yml** - Project configuration # - **.gitignore** - Git ignore rules # - # The web template pins Amber `2.0.0-beta.4`, uses ECR, and includes Grant, + # The web template pins Amber `2.0.0-beta.5`, uses ECR, and includes Grant, # Micrate-powered database commands, and the selected database driver. Model, # scaffold, and migration generators are supported. API-resource, # authentication, and native generators are preview surfaces during the beta. diff --git a/src/amber_cli/generators/native_app.cr b/src/amber_cli/generators/native_app.cr index 9b4b0f6..fad6175 100644 --- a/src/amber_cli/generators/native_app.cr +++ b/src/amber_cli/generators/native_app.cr @@ -118,7 +118,7 @@ dependencies: # Amber Framework V2 (patterns only, NO HTTP server for native apps) amber: github: amberframework/amber - version: 2.0.0-beta.4 + version: 2.0.0-beta.5 # Grant ORM (ActiveRecord-style, replaces Granite in V2) grant: diff --git a/src/amber_cli/templates/app/README.md.ecr b/src/amber_cli/templates/app/README.md.ecr index c1b4f9b..73431ab 100644 --- a/src/amber_cli/templates/app/README.md.ecr +++ b/src/amber_cli/templates/app/README.md.ecr @@ -1,6 +1,6 @@ # <%= @name %> -An ECR web application generated by Amber CLI for Amber `2.0.0-beta.4`. +An ECR web application generated by Amber CLI for Amber `2.0.0-beta.5`. ## Run it @@ -26,7 +26,7 @@ the app to target a server database. ## Production configuration Set at least `AMBER_SERVER_SECRET_KEY_BASE` and `AMBER_DATABASE_URL`. See the -[Amber V2 beta guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.4/docs/beta-installation.md). +[Amber V2 beta guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.5/docs/beta-installation.md). ## Tests diff --git a/src/amber_cli/templates/app/shard.yml.ecr b/src/amber_cli/templates/app/shard.yml.ecr index 3059fda..d82e560 100644 --- a/src/amber_cli/templates/app/shard.yml.ecr +++ b/src/amber_cli/templates/app/shard.yml.ecr @@ -15,7 +15,7 @@ targets: dependencies: amber: github: amberframework/amber - version: 2.0.0-beta.4 + version: 2.0.0-beta.5 grant: github: crimson-knight/grant commit: 2665a978b43ac608c68cde9243821f8f8f053372