New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EctorErrorSerializer not available #34
Comments
Thanks for the report @thewalkingtoast. I will look into it. |
@thewalkingtoast I am having a bit of a tough time reproducing this. I am wondering if it is a compilation issue, could you give the branch a try and see if it help? |
@alanpeabody No change. Here is the exact dump from 1) test does not update resource and renders errors when data is invalid (InterpreterServer.UserControllerTest)
test/controllers/user_controller_test.exs:130
** (UndefinedFunctionError) undefined function: JaSerializer.EctoErrorSerializer.format/1 (module JaSerializer.EctoErrorSerializer is not available)
stacktrace:
JaSerializer.EctoErrorSerializer.format(%{__struct__: Ecto.Changeset, action: :update, changes: %{first_name: "!", start_year: "0"}, conn: %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{__struct__: Ecto.Changeset, action: :update, changes: %{first_name: "!", start_year: "0"}, constraints: [], errors: [start_year: {"should be %{count} characters", [count: 4]}, first_name: "has invalid format"], filters: %{}, layout: false, model: %InterpreterServer.User{__meta__: #Ecto.Schema.Metadata<:loaded>, address: nil, avatar: nil, deleted_at: nil, first_name: "Arya", id: 1, inserted_at: #Ecto.DateTime<2015-10-27T14:40:19Z>, last_name: "Stark", miles_willing_to_travel: nil, start_month: "04", start_year: "2011", updated_at: #Ecto.DateTime<2015-10-27T14:40:19Z>}, optional: [:avatar, :miles_willing_to_travel, :deleted_at], opts: [source: :changeset], params: %{"first_name" => "!", "last_name" => "Stark", "start_month" => "04", "start_year" => "0"}, repo: InterpreterServer.Repo, required: [:first_name, :last_name, :start_year, :start_month], types: %{address: {:assoc, %Ecto.Association.Has{cardinality: :one, defaults: [], field: :address, on_cast: :changeset, on_delete: :fetch_and_delete, on_replace: :raise, owner: InterpreterServer.User, owner_key: :id, queryable: InterpreterServer.Address, related: InterpreterServer.Address, related_key: :user_id}}, avatar: :string, deleted_at: Ecto.DateTime, first_name: :string, id: :id, inserted_at: Ecto.DateTime, last_name: :string, miles_willing_to_travel: :integer, start_month: :string, start_year: :string, updated_at: Ecto.DateTime}, valid?: false, validations: [start_month: {:length, [is: 2]}, start_year: {:length, [is: 4]}, last_name: {:format, ~r/[^`!<>@#\$%\^&*+_=]+/}, first_name: {:format, ~r/[^`!<>@#\$%\^&*+_=]+/}]}, before_send: [#Function<0.61510215/1 in JaSerializer.ContentTypeNegotiation.set_content_type/2>, #Function<1.25101471/1 in Plug.Logger.call/2>], body_params: %{"data" => %{"attributes" => %{"first_name" => "!", "last_name" => "Stark", "start_month" => "04", "start_year" => "0"}, "id" => "1", "type" => "users"}}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "www.example.com", method: "PATCH", owner: #PID<0.2408.0>, params: %{"data" => %{"attributes" => %{"first_name" => "!", "last_name" => "Stark", "start_month" => "04", "start_year" => "0"}, "relationships" => nil}, "id" => "1"}, path_info: ["api", "v1", "users", "1"], peer: {{127, 0, 0, 1}, 111317}, port: 80, private: %{InterpreterServer.Router => {[], %{}}, :phoenix_action => :update, :phoenix_controller => InterpreterServer.UserController, :phoenix_endpoint => InterpreterServer.Endpoint, :phoenix_format => "json-api", :phoenix_layout => {InterpreterServer.LayoutView, :app}, :phoenix_pipelines => [:api], :phoenix_recycled => true, :phoenix_route => #Function<8.92130501/1 in InterpreterServer.Router.match/4>, :phoenix_router => InterpreterServer.Router, :phoenix_template => "error-object.json", :phoenix_view => InterpreterServer.ChangesetView, :plug_skip_csrf_protection => true}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "application/vnd.api+json"}, {"content-type", "application/vnd.api+json"}], request_path: "/api/v1/users/1", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "q8hdjseum05p8ldgivlphmf16ev5s5p8"}, {"content-type", "application/vnd.api+json; charset=utf-8"}], scheme: :http, script_name: [], secret_key_base: "4o7<snip>", state: :unset, status: 422}, constraints: [], errors: [start_year: {"should be %{count} characters", [count: 4]}, first_name: "has invalid format"], filters: %{}, model: %InterpreterServer.User{__meta__: #Ecto.Schema.Metadata<:loaded>, address: nil, avatar: nil, deleted_at: nil, first_name: "Arya", id: 1, inserted_at: #Ecto.DateTime<2015-10-27T14:40:19Z>, last_name: "Stark", miles_willing_to_travel: nil, start_month: "04", start_year: "2011", updated_at: #Ecto.DateTime<2015-10-27T14:40:19Z>}, optional: [:avatar, :miles_willing_to_travel, :deleted_at], opts: [source: :changeset], params: %{"first_name" => "!", "last_name" => "Stark", "start_month" => "04", "start_year" => "0"}, repo: InterpreterServer.Repo, required: [:first_name, :last_name, :start_year, :start_month], types: %{address: {:assoc, %Ecto.Association.Has{cardinality: :one, defaults: [], field: :address, on_cast: :changeset, on_delete: :fetch_and_delete, on_replace: :raise, owner: InterpreterServer.User, owner_key: :id, queryable: InterpreterServer.Address, related: InterpreterServer.Address, related_key: :user_id}}, avatar: :string, deleted_at: Ecto.DateTime, first_name: :string, id: :id, inserted_at: Ecto.DateTime, last_name: :string, miles_willing_to_travel: :integer, start_month: :string, start_year: :string, updated_at: Ecto.DateTime}, valid?: false, validations: [start_month: {:length, [is: 2]}, start_year: {:length, [is: 4]}, last_name: {:format, ~r/[^`!<>@#\$%\^&*+_=]+/}, first_name: {:format, ~r/[^`!<>@#\$%\^&*+_=]+/}]})
(phoenix) lib/phoenix/view.ex:341: Phoenix.View.render_to_iodata/3
(phoenix) lib/phoenix/controller.ex:628: Phoenix.Controller.do_render/4
(interpreter_server) web/controllers/user_controller.ex:1: InterpreterServer.UserController.action/2
(interpreter_server) web/controllers/user_controller.ex:1: InterpreterServer.UserController.phoenix_controller_pipeline/2
(interpreter_server) lib/phoenix/router.ex:255: InterpreterServer.Router.dispatch/2
(interpreter_server) web/router.ex:1: InterpreterServer.Router.do_call/2
(interpreter_server) lib/interpreter_server/endpoint.ex:1: InterpreterServer.Endpoint.phoenix_pipeline/1
(interpreter_server) lib/phoenix/endpoint/render_errors.ex:34: InterpreterServer.Endpoint.call/2
(phoenix) lib/phoenix/test/conn_test.ex:193: Phoenix.ConnTest.dispatch/5
test/controllers/user_controller_test.exs:139 ChangesetView: defmodule InterpreterServer.ChangesetView do
@moduledoc """
Shows Ecto changeset errors in JSON:API compliant fashion.
"""
use InterpreterServer.Web, :view
# use JaSerializer.PhoenixView defined in web/web.ex
def render("error-object.json", changeset) do
JaSerializer.EctoErrorSerializer.format(changeset)
end
end |
I was just able to trigger this locally in dev. No clear answers yet but I feel better knowing it's not just test now. Interestingly, I had ran |
Yeah, I am assuming it is some kind of compilation/dependency issue. If Ecto is not compiled it will not define the EctoErrorSerializer module, and you will get this error. I thought my branch change might effect it, but seemingly not. To be honest I am thinking about opinionating more towards Ecto and making the dependency required, not optional and removing the compilation conditionals. I am going to play with this a bit more with what you provided above to see if blowing away the deps and build helps me to re-create it. |
Thanks I was able to reproduce! Unfortunately I am a bit of a loss. The I will probably push a 0.5 release that adds Ecto as a required (non-optional) dependency this weekend. As part of that push, I may add a error serializer to the existing view, similar to what you have defined. |
@thewalkingtoast I do have a workaround to offer in the meantime: Add ecto as a dependency of your project directly, not just via phoenix_ecto. |
@alanpeabody That worked wonderful! Thank you very much for your work! 🎆 |
@thewalkingtoast I am having trouble convincing myself I should make ecto a required dependency. Do you think the direct dependency requirement is too much of a pain, especially if called out in the README? |
@alanpeabody That sounds good to me. Thanks again! |
Most people use Ecto. If there's a complaint about Ecto being required then it should be reconsidered. |
ja_serializer is a primarially a serializer for Ecto apps. Let's embrace that. This fixes #34.
I believe that making Ecto a required dependency was a mistake. There are good cases for not wanting Ecto as it pulls in so many dependencies, but beyond that having a Ecto versioned dep becomes problematic when you are dealing with other libraries. I think Ecto should be removed as a required dep. |
To better illustrate the issue, I now have to add this to my {:ecto, "> 0.0.0", override: true} |
Thanks for the feedback @bcardarella. I need to spend some time understanding how dependencies + |
I'm a +1 for removing the Ecto dependency. I would prefer to see example implementations for ecto/phoenix rather that modules in a serializer. I know that phoenix isn't a dep but having specific logic to handle a framework, or external library, seems out of place for a serializer library. I wouldn't mind contributing to the effort. |
@nurugger07 you mean encouraging people to write their own render functions in views and something to add to the error and changeset views? It really isn't much code so I am not opposed to that. Frankly my biggest concern is generating more confusion/issues around "how do I use this with phoenix?" that have to be answered, but maybe forcing explicitness will reduce that? |
@alanpeabody yes. I think the docs, README, sample apps, or even link to a blog post are all great ways to teach people how to use libraries in specific situations. It also gives them a better understanding of what's happening and how they can alter it for their needs. |
The phoenix view and error serializer piece would be easy enough. Do you have suggestions on how some of the other existing pattern matches against Ecto structs could be handled? In particular these two I don't have an answer too:
Any help, especially with documentation (something I struggle with), is very much appreciated! |
@alanpeabody I believe you can just pattern match on a map with the expected def get_data(struct, name, opts) do
rel = (opts[:field] || name)
struct
|> Map.get(rel)
|> case do
%{__struct__: Ecto.Association.NotLoaded} -> raise @error, rel: rel, name: name
other -> other
end
end I think this will allow you to get away with not having Ecto as a required dependency while still supporting this use-case if someone happens to be using Ecto |
That makes sense @bcardarella, thanks! Any ideas on the protocol implementation? My concern with removing it is that falling back to the |
Does Elixir throw a compilation error if Ecto is not defined? I would have thought that Elixir would just treat these as atoms internally? |
Doesn't appear it does raise an error. I have a branch with ecto moved to a test only dependency. I am a little unclear if moving a dependency to |
Elixir shouldn't have a problem with matching against modules that don't exist. As @bcardarella said, it'll just treat it as an atom. This is the same as when you type a made-up name into the REPL and it just returns it to you. I think the worst-case scenario is that one will haven to add their own render functions to |
Using either 0.4.0 or HEAD, under test we get
** (UndefinedFunctionError) undefined function: JaSerializer.EctoErrorSerializer.format/1 (module JaSerializer.EctoErrorSerializer is not available)
a lot. When ran indev
, it works just fine with error responses.Mix.exs:
The text was updated successfully, but these errors were encountered: