diff --git a/.credo.exs b/.credo.exs index 9965c2f..b62ab1c 100644 --- a/.credo.exs +++ b/.credo.exs @@ -84,12 +84,12 @@ # {Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, # You can also customize the exit_status of each check. # If you don't want TODO comments to cause `mix credo` to fail, just # set this value to 0 (zero). # {Credo.Check.Design.TagTODO, [exit_status: 2]}, - {Credo.Check.Design.TagFIXME, []}, # ## Readability Checks @@ -99,7 +99,7 @@ {Credo.Check.Readability.LargeNumbers, []}, {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, {Credo.Check.Readability.ModuleAttributeNames, []}, - {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleDoc, false}, {Credo.Check.Readability.ModuleNames, []}, {Credo.Check.Readability.ParenthesesInCondition, []}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, @@ -122,32 +122,35 @@ {Credo.Check.Refactor.Apply, []}, {Credo.Check.Refactor.CondStatements, []}, {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, {Credo.Check.Refactor.FunctionArity, []}, {Credo.Check.Refactor.LongQuoteBlocks, []}, - {Credo.Check.Refactor.MatchInCondition, []}, {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, {Credo.Check.Refactor.NegatedConditionsInUnless, []}, {Credo.Check.Refactor.NegatedConditionsWithElse, []}, {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, {Credo.Check.Refactor.UnlessWithElse, []}, {Credo.Check.Refactor.WithClauses, []}, - {Credo.Check.Refactor.FilterFilter, []}, - {Credo.Check.Refactor.RejectReject, []}, - {Credo.Check.Refactor.RedundantWithClauseResult, []}, # ## Warnings # {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, {Credo.Check.Warning.IExPry, []}, {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, {Credo.Check.Warning.OperationOnSameValues, []}, {Credo.Check.Warning.OperationWithConstantResult, []}, {Credo.Check.Warning.RaiseInsideRescue, []}, {Credo.Check.Warning.SpecWithStruct, []}, - {Credo.Check.Warning.WrongTestFileExtension, []}, + {Credo.Check.Warning.UnsafeExec, []}, {Credo.Check.Warning.UnusedEnumOperation, []}, {Credo.Check.Warning.UnusedFileOperation, []}, {Credo.Check.Warning.UnusedKeywordOperation, []}, @@ -156,11 +159,12 @@ {Credo.Check.Warning.UnusedRegexOperation, []}, {Credo.Check.Warning.UnusedStringOperation, []}, {Credo.Check.Warning.UnusedTupleOperation, []}, - {Credo.Check.Warning.UnsafeExec, []} + {Credo.Check.Warning.WrongTestFileExtension, []} ], disabled: [ # - # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, # # Controversial and experimental checks (opt-in, just move the check to `:enabled` @@ -175,6 +179,8 @@ {Credo.Check.Readability.ImplTrue, []}, {Credo.Check.Readability.MultiAlias, []}, {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, {Credo.Check.Readability.SeparateAliasRequire, []}, {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, {Credo.Check.Readability.SinglePipe, []}, @@ -189,6 +195,7 @@ {Credo.Check.Refactor.MapMap, []}, {Credo.Check.Refactor.ModuleDependencies, []}, {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, {Credo.Check.Refactor.PipeChainStart, []}, {Credo.Check.Refactor.RejectFilter, []}, {Credo.Check.Refactor.VariableRebinding, []}, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5931fef..1ac2f2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,9 @@ name: CI +env: + ELIXIR_VERSION: "1.16" + OTP_VERSION: "26" + on: push: branches: @@ -11,14 +15,18 @@ on: jobs: test: runs-on: ubuntu-latest - name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + name: Test OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: - otp: ["24.3", "25.1"] - elixir: ["1.13", "1.14"] - exclude: - - otp: "25.1" - elixir: "1.13" + include: + - { elixir: 1.15, otp: 24 } + - { elixir: 1.15, otp: 25 } + - { elixir: 1.15, otp: 26 } + - { elixir: 1.16, otp: 24 } + - { elixir: 1.16, otp: 25 } + - { elixir: 1.16, otp: 26 } + - { elixir: 1.18, otp: 27 } + - { elixir: 1.19, otp: 27 } env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -37,13 +45,43 @@ jobs: - 5432:5432 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 + id: beam with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - name: Restore dependencies and build cache - uses: actions/cache@v3 + uses: actions/cache@v4 + with: + path: | + _build + deps + key: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}- + - name: Install Dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + - name: Run Tests + run: mix coveralls.github + + lint: + runs-on: ubuntu-latest + name: Lint + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + id: beam + with: + otp-version: ${{ env.OTP_VERSION }} + elixir-version: ${{ env.ELIXIR_VERSION}} + - name: Restore dependencies and build cache + uses: actions/cache@v4 with: path: | _build @@ -51,7 +89,7 @@ jobs: key: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles('mix.lock') }} restore-keys: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}- - name: Restore PLT cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: plt_cache with: key: | @@ -75,5 +113,3 @@ jobs: run: mix hex.audit - name: Generate docs run: mix docs - - name: Run Tests - run: mix coveralls.github diff --git a/.gitignore b/.gitignore index 03d70f1..2cef7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,9 @@ polymorphic_embed-*.tar # Ignore Dialyzer plts file .plts + +.elixir_ls +.vscode + +tmp.* +tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e213f..bf62aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,42 @@ # Changelog +## 5.0.2 + + * Remove warning when aliases cannot be expanded (#123). + +## 5.0.1 + + * Relax `phoenix_live_view` constraint to allow 1.0. + +## 5.0.x + + * Fix usage with Ecto 3.12. Require Ecto 3.12 or later + * Allow inferring type from parent field via new :use_parent_field_for_type option + +## 4.1.x + + * Add `Form.source_data/1` and `Form.source_module/1` (`get_polymorphic_type/2` doesn't work for list of embeds) + * Add `:retain_unlisted_types_on_load` and `:nilify_unlisted_types_on_load` options + * MongoDB fix + * Deprecate `:type_field` in favor of `:type_field_name` + +## 4.0.x + + * Support `sort_param` and `drop_param` for list of embeds + * Add `PolymorphicEmbed.HTML.Component.polymorphic_embed_inputs_for/1` + (similar to `Phoenix.Component.inputs_for/1`) + * Support updating list of embeds while retaining ids + * Fix form input rendering for list of embeds + * Fix `traverse_errors` for nested embeds + + **Breaking Change**: The form helper `get_polymorphic_type/3` has been updated to `get_polymorphic_type/2`. + The module name parameter (previously the second parameter) has been removed. + ## 3.0.x * Default value for polymorphic list of embeds is `[]` instead of `nil` (following `embeds_many/3`) + * Support Phoenix HTML 4.0 + * Avoid compile-time dependencies between parent and polymorphic embedded schemas ### Migration from 2.x to 3.x diff --git a/LICENSE b/LICENSE index 246040d..d9a10c0 100644 --- a/LICENSE +++ b/LICENSE @@ -174,17 +174,3 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - - Copyright 2020 Mathieu Decaffmeyer - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index ef3c9b7..28c7bfc 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ defmodule MyApp.Channel.Email do def changeset(email, params) do email - |> cast(params, ~w(address confirmed)a) + |> cast(params, [:address, :confirmed]) |> validate_required(:address) |> validate_length(:address, min: 4) end @@ -72,12 +72,19 @@ end ```elixir defmodule MyApp.Channel.SMS do use Ecto.Schema + import Ecto.Changeset @primary_key false embedded_schema do field :number, :string end + + def changeset(sms, params) do + sms + |> cast(params, [:number]) + |> validate_required(:number) + end end ``` @@ -87,7 +94,7 @@ In your migration file, you may use the type `:map` for both `polymorphic_embeds add(:channel, :map) ``` -[It is not recommended](https://hexdocs.pm/ecto/3.8.4/Ecto.Schema.html#embeds_many/3) to use `{:array, :map}` for a list of embeds. +[It is not recommended](https://hexdocs.pm/ecto/3.11.2/Ecto.Schema.html#embeds_many/3) to use `{:array, :map}` for a list of embeds. ### `cast_polymorphic_embed/3` @@ -97,17 +104,23 @@ add(:channel, :map) * `:required` – if the embed is a required field. -* `:with` – allows you to specify a custom changeset. Either pass an MFA or a function: +* `:with` – allows you to specify a custom changeset. ```elixir changeset |> cast_polymorphic_embed(:channel, with: [ - sms: {SMS, :custom_changeset, ["hello"]}, + sms: &SMS.custom_changeset/2, email: &Email.custom_changeset/2 ] ) ``` +* `:drop_param` – see [sorting-and-deleting-from-many-collections](https://hexdocs.pm/ecto/3.11.2/Ecto.Changeset.html#cast_assoc/3-sorting-and-deleting-from-many-collections). + +* `:sort_param` – see [sorting-and-deleting-from-many-collections](https://hexdocs.pm/ecto/3.11.2/Ecto.Changeset.html#cast_assoc/3-sorting-and-deleting-from-many-collections). + +* `:default_type_on_sort_create` – in some cases, [`sort` creates a new entry](https://github.com/elixir-ecto/ecto/blob/v3.11/test/ecto/changeset/embedded_test.exs#L464); this option specifies which type to use by default for the entry. + ### PolymorphicEmbed Ecto type The `:types` option for the `PolymorphicEmbed` custom type contains a keyword list mapping an atom representing the type @@ -148,14 +161,16 @@ polymorphic_embeds_many :contexts, age: MyApp.Context.Age, device: MyApp.Context.Device ], + on_replace: :delete, on_type_not_found: :raise, - on_replace: :delete + nilify_unlisted_types_on_load: [:deprecated_type] ``` #### Options * `:types` – discussed above. -* `:type_field` – specify a custom type field. Defaults to `:__type__`. +* `:type_field_name` – specify a custom field name for the field holding the type. Defaults to `:__type__`. +* `:use_parent_field_for_type` – fetch the type from a specified field in the parent schema (the schema holding the embed). * `:on_type_not_found` – specify what to do if the embed's type cannot be inferred. Possible values are - `:raise`: raise an error @@ -166,12 +181,14 @@ polymorphic_embeds_many :contexts, By default, a changeset error "is invalid" is added. * `:on_replace` – mandatory option that can only be set to `:update` for a single embed and `:delete` for a list of embeds (we force a value as the default value of this option for `embeds_one` and `embeds_many` is `:raise`). +* `:retain_unlisted_types_on_load`: allow unconfigured types to be loaded without raising an error. Useful for handling deprecated structs still present in the database. +* `:nilify_unlisted_types_on_load`: same as `:retain_unlisted_types_on_load`, but nilify the struct on load. ### Displaying form inputs and errors in Phoenix templates The library comes with a form helper in order to build form inputs for polymorphic embeds and display changeset errors. -In the entrypoint defining your web interface (`lib/your_app_web.ex` file), add the following import: +In the entrypoint defining your web interface (`lib/your_app_web.ex` file), add the following import, change `view` to the one your component use: ```elixir def view do @@ -190,7 +207,7 @@ Here is an example form using the imported function: <%= inputs_for f, :reminders, fn reminder_form -> %> <%= polymorphic_embed_inputs_for reminder_form, :channel, :sms, fn sms_form -> %>
-