Phoenix emits "protocol Enumerable not implemented" error when path helpers get called - phoenix-framework

For a Phoenix app, I created two routes as following:
scope "/", Greeter do
pipe_through :browser
get "/hello", HelloController, :show
get "/hello/:name", HelloController, :show
end
With them, the app can respond to the both "/hello" and "/hello/alice" paths.
But, when I use the path helper hello_path(#conn, :show, "alice") to produce "/hello/alice", Phoenix server emits this error message:
protocol Enumerable not implemented for "alice"
The cause is simple.
The first route creates two helpers hello_path/2 and hello_path/3, but the second route creates only one helper hello_path/4 because the hello_path/3 is already defined.
This hello_path/3 demands an enumerable as the third argument.
How should I avoid this error?

You can give one of the route a different name using as::
get "/hello", HelloController, :show
get "/hello/:name", HelloController, :show, as: :hello_with_name
Test:
iex(1)> import MyApp.Router.Helpers
MyApp.Router.Helpers
iex(2)> hello_path MyApp.Endpoint, :show
"/hello"
iex(3)> hello_path MyApp.Endpoint, :show, foo: "bar"
"/hello?foo=bar"
iex(4)> hello_with_name_path MyApp.Endpoint, :show, "alice"
"/hello/alice"
iex(5)> hello_with_name_path MyApp.Endpoint, :show, "alice", foo: "bar"
"/hello/alice?foo=bar"

Related

How to check the arguments of the auto generated Phoenix paths

No helper clause for Api.Router.Helpers.v1_user_organization_path
defined for action :show with arity 3. Please check that the function,
arity and action are correct. The following v1_user_organization_path
actions are defined under your router:
* :create
* :index
* :show
* :update
router.ex
defmodule Api.Router do
use Api.Web, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/", Api do
pipe_through :api
end
scope "/v1", Api.V1, as: :v1 do
pipe_through :api
resources "/users", UserController, only: [:create, :show, :update] do
resources "/organizations", OrganizationController, only: [:create, :update, :index, :show]
end
end
end
and when I do mix phoenix.routes I see the following v1_user_organization_path getting generated. The problem is I don't know how to use it and I don't know what I should pass into it. Is there a way I can check what this generated method accepts?
The error I get is occuring here
organization_controller.ex
def create(conn, %{"user_id" => user_id, "organization" => organization_params}) do
changeset = Organization.changeset(%Organization{}, organization_params)
case Repo.insert(changeset) do
{:ok, organization} ->
conn
|> put_status(:created)
|> put_resp_header("location", v1_user_organization_path(conn, :show, organization))
|> render("show.json", organization: organization)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(Api.ChangesetView, "error.json", changeset: changeset)
end
end
At put_resp_header("location", v1_user_organization_path(conn, :show, organization))
You can look at the auto generated documentation for the router helper functions using h in iex -S mix and reading the output of mix phoenix.routes for some help. For example, for the following routes:
resources "/posts", PostController do
resources "/comments", CommentController
end
I get:
iex(1)> h MyApp.Router.Helpers.post_comment_path
def post_comment_path(conn_or_endpoint, action, post_id)
def post_comment_path(conn_or_endpoint, action, post_id, params)
def post_comment_path(conn_or_endpoint, action, post_id, id, params)
$ mix phoenix.routes
post_comment_path GET /posts/:post_id/comments MyApp.CommentController :index
post_comment_path GET /posts/:post_id/comments/:id/edit MyApp.CommentController :edit
post_comment_path GET /posts/:post_id/comments/new MyApp.CommentController :new
post_comment_path GET /posts/:post_id/comments/:id MyApp.CommentController :show
post_comment_path POST /posts/:post_id/comments MyApp.CommentController :create
post_comment_path PATCH /posts/:post_id/comments/:id MyApp.CommentController :update
PUT /posts/:post_id/comments/:id MyApp.CommentController :update
post_comment_path DELETE /posts/:post_id/comments/:id MyApp.CommentController :delete
It's not clear from just the function signature which action accepts how many arguments, but if you read the output of mix phoenix.routes, you can see that :show (last column) requires a post_id and an id.
The output of h is also not completely accurate because it doesn't tell you that the arity 4 version also accepts (conn_or_endpoint, action, post_id, id) and not just (conn_or_endpoint, action, post_id, params).
I don't think there's any better auto generated documentation for the generated route functions right now in Phoenix. I usually just look at the output of mix phoenix.routes and pass in conn_or_endpoint followed by the action followed by every :var in the route, optionally followed by a params map.

phoenix view module not available

My application works and api json requests and for regular html. My router.ex
defmodule MyApp.Router do
use MyApp.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", MyApp do
pipe_through :api # Use the default browser stack
scope "/v1", V1, as: :v1 do
resources "/users", UserController, except: [:new, :edit, :index]
end
end
scope "/", MyApp do
pipe_through :browser # Use the default browser stack
get "/confirm/:token", UserController, :confirm, as: :user_confirm
end
end
my web/controllers/v1/user_controller.ex
defmodule MyApp.V1.UserController do
use MyApp.Web, :controller
def create(conn, %{"user" => user_params}) do
...
conn
|> put_status(:created)
|> put_resp_header("location", v1_user_path(conn, :show, user))
|> render("sign_up.json", user: Map.put(user, :session, result[:session]))
...
end
and my web/controllers/user_controller.rb
defmodule MyApp.UserController do
use MyApp.Web, :controller
alias MyApp.User
def confirm(conn, %{"token" => token}) do
...
render(conn, "confirmed.html")
...
end
end
my web/views/v1/user_view.ex
defmodule MyApp.V1.UserView do
use MyApp.Web, :view
...
end
and my web/views/user_view.ex
defmodule MyApp.UserView do
use MyApp.Web, :view
end
Everything works fine until I added a route and a controller for html.
Now, when I make a request for api json, I get an error
Request: POST /api/v1/users
** (exit) an exception was raised:
** (UndefinedFunctionError) function MyApp.V1.UserView.render/2 is undefined (module MyApp.V1.UserView is not available)
But if I delete web/vews/user_view.ex, then this query works without errors.
How can you correct this error?
These types of errors can usually be resolved by running mix clean. You may also see this type of error during Live code reload in dev. It case, try restarting the Phoenix.Server, and if that does not help, run mix clean

routing resources with a wild card

I have this routing
scope "/api", MosaicApi do
pipe_through :api
# resources "/cards", CardController, except: [:new, :edit]
resources "/estimates/:product", EstimateController, except: [:new, :edit]
Initially I used the generated CardController and things worked (at least POST/create did), but now I want to generalise as Card is a product type and I have a variety of other products that need to expose the exact same CRUD operations. So I am trying to morph Card* to Estimate*
In EstimateController I now have this
defmodule Api.CardController do
use Api.Web, :controller
alias Api.Card
def create(conn, %{"product" => product}) do
conn
|> render("result.json", product: product)
end
...
What I want to do is pattern match on product to bring into scope the relevant Struct (Card, ...), but I've got stuck as the code above yields this error
undefined function Api.EstimateController.init/1 (module Api.EstimateController is not available)
Api.EstimateController.init(:create)
I'm confused as init is not mentioned in http://www.phoenixframework.org/docs/controllers at all
Other indications that things are mostly good
mix phoenix.routes
page_path GET / Api.PageController :index
estimate_path GET /api/estimates/:product Api.EstimateController :index
estimate_path GET /api/estimates/:product/:id Api.EstimateController :show
estimate_path POST /api/estimates/:product Api.EstimateController :create
The function init/1 is defined in https://github.com/phoenixframework/phoenix/blob/v1.1.4/lib/phoenix/controller/pipeline.ex#L98
def init(action) when is_atom(action) do
action
end
It looks like you are not calling use Phoenix.Controller in your EstimatesController.
Usually this is done with:
use Api.Web, :controller

Passing a route path as a method parameter without evaluating it first

I have a Organization model with a simple validates :name, presence: true validation.
To DRY up some code I have a "helper" method in my ApplicationController as follows:
def handle_errors(object, message, origin, target)
if object.invalid?
yield if block_given?
render(origin)
else
redirect_to(target, flash: { notice: message })
end
end
This works very well on the update action:
#organization = Organization.find(params[:id])
#organization.update(organization_params)
handle_errors #organization, "Updated organization", 'edit', organization_path(#organization)
However, in the create action
#organization = Organization.new(organization_params)
#organization.save
handle_errors #organization, "Updated organization", 'new', organization_path(#organization)
it breaks when you fail the validation (i.e., enter a blank name-field) with the error
No route matches {:action=>"show", :controller=>"organizations", :id=>nil} missing required keys: [:id]
The id-key is missing because #organization is unsaved and #organization.id = nil. Since the route path method is evaluated before it's passed to the handle_errors method it generates an error.
What I would like to do is to "evaluate the route path" later (only in the else-case of the handle_errors method). One way to do this would be to wrap the route path in a lambda block and call the method as follows:
handle_errors #organization, "Updated organization", 'new', lambda {organization_path(#organization)}
Yet another way to solve the problem would be to instead of passing the route path to the handle_errors method is to just pass the object (or an array for nested resources and custom actions-name)
handle_errors #organization, "Updated organization", 'new', [#organization]
But is there any other way to do this in an even more ruby/rails oriented way?

Verb-agnostic matching in Sinatra

We can write
get '/foo' do
...
end
and
post '/foo' do
...
end
which is fine. But can I combine multiple HTTP verbs in one route?
This is possible via the multi-route extension that is part of sinatra-contrib:
require 'sinatra'
require "sinatra/multi_route"
route :get, :post, '/foo' do
# "GET" or "POST"
p request.env["REQUEST_METHOD"]
end
# Or for module-style applications
class MyApp < Sinatra::Base
register Sinatra::MultiRoute
route :get, :post, '/foo' do
# ...
end
end
However, note that you can do this simply yourself without the extension via:
foo = lambda do
# Your route here
end
get '/foo', &foo
post '/foo', &foo
Or more elegantly as a meta-method:
def self.get_or_post(url,&block)
get(url,&block)
post(url,&block)
end
get_or_post '/foo' do
# ...
end
You might also be interested in this discussion on the feature.
FWIW, I just do it manually, with no helper methods or extensions:
%i(get post).each do |method|
send method, '/foo' do
...
end
end
Although if you're doing it a lot it of course makes sense to abstract that out.
Phrogz has a great answer, but if either lambdas or including sinatra-contrib isn't for you, then this meta method will achieve the same result as sinatra-contrib for your purposes:
# Provides a way to handle multiple HTTP verbs with a single block
#
# #example
# route :get, :post, '/something' do
# # Handle your route here
# end
def self.route(*methods, path, &block)
methods.each do |method|
method.to_sym
self.send method, path, &block
end
end
If you're a little wary of being able to send arbitrary methods to self, then you can protect yourself by setting up a whitelist of allowed methods in an array, and then checking for the symbol's presence in the array.
# Provides a way to handle multiple HTTP verbs with a single block
#
# #example
# route :get, :post, '/something' do
# # Handle your route here
# end
def self.route(*methods, path, &block)
allowed_methods = [:get, :post, :delete, :patch, :put, :head, :options]
methods.each do |method|
method.to_sym
self.send(method, path, &block) if allowed_methods.include? method
end
end
Here's a service-unavailable server that I managed to get on single line :)
require 'sinatra';set port: ARGV[0]||80;%w.get post put patch options delete..map{|v|send(v,'*'){503}}
I actually used this to test the behavior of some client code in the face of 503s.

Resources