Support URL-encoded/multipart HTML form values for deeply nested SaveOperations - #1156
Draft
paulcsmith with Copilot wants to merge 15 commits into
Draft
paulcsmith with Copilot wants to merge 15 commits into
paulcsmith with Copilot wants to merge 15 commits into
Conversation
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
…lper Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
…Operations Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
paulcsmith
August 19, 2026 12:03
View session
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
…mantics Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
Co-authored-by: paulcsmith <22394+paulcsmith@users.noreply.github.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds full support for nested
has_one/has_manySaveOperations -- similar to Rails'accepts_nested_attributes_for-- so you can save/update/destroy an entire tree of records (e.g. aPostand all of itsComments) through a single parentSaveOperation, in a type-safe way:has_many/has_onemacros for declaring nested child operations, nestable arbitrarily deep (ahas_manychild can itself declare its ownhas_one/has_many)Avram::ParamKeyOverride#param_keysotext_input,nested_id_input, etc. render the exactname=/id=needed for round-tripping through a formAvram::Params#nested/#many_nesteddecode both JSON payloads and URL-encoded/multipart HTML form field naming conventionsallow_destroy: trueonhas_manylets a submitted item delete its associated record instead of saving itExample
Forms, with Lucky helpers
This renders
name="comments[0]:body"/id="comments_0_body"for the first comment,name="comments[1]:body"for the second, and so on -- and pre-fills from the post's existing comments when editing a persistedPostwith no submitted params.JSON requests
The same data works from a JSON API client, no special encoding required:
{ "title": "My Post", "comments": [ { "body": "First" }, { "id": "42", "body": "Updated existing comment" } ] }This is equivalent to submitting
"comments[0]:body"and"comments[1]:id"/"comments[1]:body"as separate form fields --Avram::Paramsdecodes both conventions into the same nested params.Validations
If any comment fails validation (e.g. a blank
body), the parentoperation.valid?isfalseand nothing is saved -- not the post, and not any of the other comments, even ones that are individually valid. An error is added to the parent under:comments, while each comment's own errors remain available for re-rendering the form:Create vs. update
Whether an item in a
has_manycreates a brand-new record or updates an existing one is decided per item, based on whether its hash includes anidthat matches one of the parent's already-associated records -- not by whethercreateorupdateis called on the parent operation:has_oneworks the same way, minus theidmatching -- since there's only ever one associated record, it's always that one that gets updated (or created, if none exists yet).This PR's specific fix: URL-encoded/multipart HTML form values for deeply nested SaveOperations
Deeply nested
has_one/has_manySaveOperationsupport only worked for JSON request bodies.has_many's macro re-wraps each array item's flat params into a freshAvram::Paramsfor its own childSaveOperation, but that re-wrapped params object only knew how to decode the JSON-string nesting convention. Real HTML forms (andLucky::Paramsitself) encode nested fields differently, so grandchild-level associations submitted via a form silently received wrong or empty data. None of the existing nested-save-operation specs exercised the realAvram::Paramsclass, so this gap went uncaught.Fix
Avram::Params#nested/#many_nestednow also recognize the form/URL-encoded naming convention used byLucky::Params:"key:field"for a nestedhas_one-style value, and"key[index]:field"forhas_many-style items.many_nestedgroups form keys by index and sorts numerically, so submission order doesn't affect item ordering.Tests
params_spec.crcovering form-key extraction, grouping, index ordering, and JSON-over-form-key precedence.spec/avram/operations/nested_save_operation_real_params_spec.cr, an integration spec that builds a realAvram::Params(rather than the existingFakeDeeplyNestedParamstest double) from both JSON and URL-encoded-style flat hashes, provinghas_many-in-has_many,has_one-in-has_many, andhas_many-in-has_onesave correctly end-to-end, including a rollback case.spec/lucky/ext/nested_operation_rendering_spec.crcovering the rendering side (correctname=/id=for forms, including deeply-nested cases).Docs
Avram::Params#nested/#many_nested, thehas_many/has_onemacros, andFakeDeeplyNestedParamsto describe the JSON and form-key conventions, validations, JSON request examples, and create-vs-update semantics.