3

I am copying Arc.Ecto changeset example https://github.com/stavro/arc_ecto and I am not sure about the typespecs, I am trying with these but doesnt seem to work for dialyzer, it complains on |> cast_attachments(params, [:avatar])

@spec changeset(Ecto.Schema.t, map | :invalid) :: Changeset.t
def changeset(user, params \\ :invalid) do
  user
  |> cast(params, [:name])
  |> cast_attachments(params, [:avatar])
  |> validate_required([:name, :avatar])
end


> The pattern #{'__meta__':=_} can never match the type
> #{'__struct__':='Elixir.Ecto.Changeset', 'action':='delete' | 'insert' | 'nil' | 'replace' | 'update', 'changes':=#{atom()=>_},
> 'constraints':=[#{'constraint':=binary(), 'field':=atom(),
> 'match':='exact' | 'suffix', 'message':={binary(),[{atom(),_}]},
> 'type':='unique'}], 'data':='nil' | map(), 'empty_values':=_,
> 'errors':=[{atom(),{binary(),[{atom(),_}]}}], 'filters':=#{atom()=>_},
> 'params':='nil' | #{binary()=>_},
> 'prepare':=[fun((#{'__struct__':='Elixir.Ecto.Changeset',
> 'action':='delete' | 'insert' | 'nil' | 'replace' | 'update',
> 'changes':=map(), 'constraints':=[any()], _=>_}) ->
> #{'__struct__':='Elixir.Ecto.Changeset', 'action':='delete' | 'insert' | 'nil' | 'replace' | 'update', 'changes':=map(),
> 'constraints':=[any()], _=>_})], 'repo':=atom(), 'required':=[atom()],
> 'types':='nil' | #{atom()=>atom() | {'array',_} |
> {'embed',#{'__struct__':='Elixir.Ecto.Embedded', 'cardinality':='many'
> | 'one', 'field':=atom(), 'on_cast':='nil' | fun(),
> 'on_replace':='delete' | 'mark_as_invalid' | 'raise', 'owner':=atom(),
> 'related':=atom(), 'unique':=boolean()}} | {'in',_} | {'map',_}},
> 'valid?':=boolean(), 'validations':=[{atom(),_}]}

cast_attachments is a macro so I am not sure how to define a typespec for a macro since in it's source code hasnt any spec defined https://github.com/stavro/arc_ecto/blob/master/lib/arc_ecto/schema.ex

lapinkoira
  • 6,688
  • 7
  • 36
  • 74
  • Did you manage to solve this? I have the same issue... – João Almeida Apr 20 '18 at 09:37
  • Sadly arc_ecto is not actively maintained and one of the things which is lacking is to implement typespecs in its modules and macros, thats why it fails when running dialyzer... couldnt fix this myself, I think its an arc_ecto issue. Let me know if you manage to fix it.. – lapinkoira Apr 20 '18 at 10:13

1 Answers1

2

The error is caused by the case statement that checks the type of changeset_or_data. Since it is a macro, when dialyzer checks it after compiling, changeset_or_data has been defined in the calling module and so dialyzer knows what type it is, so one of the case statement clauses can never be reached. That's how I understand it...in any case moving the logic in the case statement out into functions seems to fix it:

  defmacro cast_attachments(changeset_or_data, params, allowed, options \\ []) do
    quote bind_quoted: [changeset_or_data: changeset_or_data,
                        params: params,
                        allowed: allowed,
                        options: options] do

      # If given a changeset, apply the changes to obtain the underlying data

      scope = do_apply_changes(changeset_or_data)

      # Cast supports both atom and string keys, ensure we're matching on both.
      allowed_param_keys = Enum.map(allowed, fn key ->
        case key do
          key when is_binary(key) -> key
          key when is_atom(key) -> Atom.to_string(key)
        end
      end)

      arc_params = case params do
        :invalid ->
          :invalid
        %{} ->
          params
          |> Arc.Ecto.Schema.convert_params_to_binary
          |> Map.take(allowed_param_keys)
          |> Enum.reduce([], fn
            # Don't wrap nil casts in the scope object
            {field, nil}, fields -> [{field, nil} | fields]

            # Allow casting Plug.Uploads
            {field, upload = %{__struct__: Plug.Upload}}, fields -> [{field, {upload, scope}} | fields]

            # If casting a binary (path), ensure we've explicitly allowed paths
            {field, path}, fields when is_binary(path) ->
              if Keyword.get(options, :allow_paths, false) do
                [{field, {path, scope}} | fields]
              else
                fields
              end
          end)
          |> Enum.into(%{})
      end

      cast(changeset_or_data, arc_params, allowed)
    end
  end

  def do_apply_changes(%Ecto.Changeset{} = changeset), do: Ecto.Changeset.apply_changes(changeset)
  def do_apply_changes(%{__meta__: _} = data), do: data

Note the new do_apply_changes function at the end

There's a fork here with a fix - https://github.com/madebymany/arc_ecto

I've put in a pull request to get it merged in - https://github.com/stavro/arc_ecto/pull/106

Kat Lynch
  • 21
  • 3
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. – Anh Pham Dec 04 '18 at 16:08
  • @AnhPham thanks! Have updated with the code and an explanation – Kat Lynch Dec 05 '18 at 16:25