Closed SachsKaylee closed 3 years ago
Howdy! Sorry for the delayed response here.
I take it you've already looked into json/2
? It's designed for this exact use case of using Sugar for REST API backends (following code should work in theory, but my Elixir's a bit rusty at the moment :wink:):
defmodule MyApp.Controllers.Customer do
use Sugar.Controller
# ...
def index(conn) do
data = MyApp.Customer.get_all() # or whatever
conn |> json(data |> get_customers)
end
def show(conn, id) do
data = MyApp.Customer.get(id) # or whatever
conn |> json(data |> get_customer)
end
# ...
defp get_customer(data) do
contracts = MyApp.Customer.get_recent_contracts(data)
[ id: data.id,
name: data.name,
recent_contract_count: length(contracts) ]
end
defp get_customers(data) do
data |> Enum.map(&(get_customer(&1)))
end
end
render/4
is really less about "views" and more specifically about "templates" - namely, about generating actual HTML from template files (be they HTML w/ EEx or HAML). json/2
is (IMO) the better choice for this, since it's more intelligent about e.g. setting Content-Type
to application/json
and all that jazz.
You also probably want to keep things like MyApp.Customer.get_recent_contracts/1
in your controllers (per above) rather than your views from an MVC purism standpoint, but I'm just nitpicky like that š
Thanks for the response!
I ended up writing a small helper module that allows me to write pretty much the syntax posted in my initial post above. I didn't end up going with Sugar though since I noticed that with this feature in place I did not actually need a wrapper around plug.
I take it you've already looked into json/2? It's designed for this exact use case of using Sugar for REST API backends (following code should work in theory, but my Elixir's a bit rusty at the moment wink):
I did look into json/2 - The feature I was missing (or maybe I just missed it) was a way to declare the way my data should be converted to JSON. E.g. I have a MyApp.Customer struct and I want to send it as JSON. However the struct may have some fields that I don't want to send (maybe it contains sensitive information or just plain isn't required to be sent) or need to further transformation that I don't feel like it belongs in the controller.
The way you posted the example is pretty much how the app used to convert the data to JSON before the initial post. But this always seemed like a smell to me because it didn't seem to logically belong in the controller (or the "plug") and made it hard to re-use the functions between controllers (calling a conversion function from another plug always seemed wrong)
For this reason I rather like the ability of phoenix to create views that output JSON because you can declare once how e.g. a customer.json should look like and re-use this function many times. Otherwise I'd either have repeat this code/function call in every single controller that responds with the same data structure.
render/4 is really less about "views" and more specifically about "templates" - namely, about generating actual HTML from template files (be they HTML w/ EEx or HAML). json/2 is (IMO) the better choice for this, since it's more intelligent about e.g. setting Content-Type to application/json and all that jazz.
I fully understand this decision and the rationale behind this.
You also probably want to keep things like MyApp.Customer.get_recent_contracts/1 in your controllers (per above) rather than your views from an MVC purism standpoint, but I'm just nitpicky like that smile
You are absolutely right, that's much better :D
I'm going to close this issue since it's no longer an important topic for my use case, and sugar does support this feature, but just takes a different "philosophical" approach to it. :)
Thanks for your time!
Thanks for your time!
No problem! And glad to hear you were able to put together something that works for you :)
(I'd definitely be interested in seeing what you've got going for that helper module; it does indeed sound like we've got two different philosophies here, but if there's anything Sugar can do to help streamline that use case a bit, I figure it'd be useful to at least investigate š)
E.g. I have a MyApp.Customer struct and I want to send it as JSON. However the struct may have some fields that I don't want to send (maybe it contains sensitive information or just plain isn't required to be sent) or need to further transformation that I don't feel like it belongs in the controller.
Your feelings are probably correct here. That said, I ain't sure I'd put that logic in the view, either; this seems to be more the purview of a separate layer between the two, like a presenter or view model / binder. Ideally the view should be doing as little as possible beyond faithfully presenting exactly what it received from its supporting layers.
Looking at REST-API-specific frameworks like Grape, we could probably take inspiration from that ecosystem's concept of "entities", which seems to describe exactly what you seek: you "present" a model/struct using an entity, and that entity then does the necessary transformations and field trimming prior to JSON output.
Sure! My plugs currently looks something something like this (all modules have been simplified, they should work in this form):
defmodule MyApp.Endpoints.Customer.Controller do
use Plug.Router
use MyApp.Controller, view: MyApp.Endpoints.Customer.View
require Logger
plug :match
plug :dispatch
get "/customers" do
customers = MyApp.Customer.get_customers()
conn
|> render("customers.json", customers: customers)
end
end
The render/2
function is imported from the MyApp.Controller
and in the background calls MyApp.Endpoints.Customer.View.render/2
:
defmodule MyApp.Endpoints.Customer.View do
def render("customers.json", customers: customers) do
customers
|> Enum.map(&render("customer.json", customer: &1))
end
def render("customer.json", customer: customer) do
%{
id: customer.id,
name: customer.name,
description: customer.description,
support_level: customer.support_level
}
end
end
While the MyApp.Controller (simplified) looks like this:
defmodule MyApp.Controller do
defmacro __using__(opts) do
render_fn = quote do
@doc """
Renders the given data in the default view of this controller.
"""
def render(conn, template, assigns), do: render(conn, unquote(view), template, assigns)
end
quote do
import MyApp.Controller, only: [render: 4]
unquote(render_fn)
end
end
@doc """
Renders the given data.
"""
@spec render(conn :: Plug.Conn.t(), view :: module, template :: atom | String.t, term) :: Plug.Conn.t()
def render(conn, view_module, template, assigns) do
res = view_module.render(template, assigns)
format = template_format(template)
encoder = get_encoder!(format)
data = encoder.(res)
conn
|> ensure_resp_content_type(MIME.type(format))
|> Plug.Conn.send_resp(conn.status || :ok, data)
end
defp get_encoder!("json"), do: Jason
defp get_encoder!(ext), do: raise ~s(no content type specified for extension "#{ext}")
defp template_format(template) do
case Path.extname(template) do
"" -> "json"
"." <> ext -> ext
end
end
defp ensure_resp_content_type(%Plug.Conn{resp_headers: resp_headers} = conn, content_type) do
if List.keyfind(resp_headers, "content-type", 0) do
conn
else
content_type = content_type <> "; charset=utf-8"
%Plug.Conn{conn | resp_headers: [{"content-type", content_type}|resp_headers]}
end
end
end
I hope that wasn't too big of a wall of code for a GitHub comment.
Your feelings are probably correct here. That said, I ain't sure I'd put that logic in the view, either; this seems to be more the purview of a separate layer between the two, like a presenter or view model / binder. Ideally the view should be doing as little as possible beyond faithfully presenting exactly what it received from its supporting layers.
You are most certainly correct here. I tend to over-use views a lot in my applications. I literally have views that generate e-mails :D This is working out really well from a productivity/iteration speed point of view since, but from a pure technical aspect the views may be impure and potentially have small side effects.
Looking at REST-API-specific frameworks like Grape, we could probably take inspiration from that ecosystem's concept of "entities", which seems to describe exactly what you seek: you "present" a model/struct using an entity, and that entity then does the necessary transformations and field trimming prior to JSON output.
This looks like an interesting approach! Especially since it looks very declarative. Also the somewhat object oriented approach can be very useful for expressing data structures since they often tend to share a lot of properties. My only concern is that it looks like it would introduce "yet another DSL". I think such a feature would probably warrant a separate library from Sugar since it would have a lot of uses on its own and probably "bloat" this minimalist framework too much.
Hello there!
Sugar looks like a really interesting and simple to use framework.
My current situation
I am interested in migrating a pure Plug project over to Sugar, but there is one big feature I'd need which I didn't see any documentation about and a quick glimpse in the source code looks like this isn't supported ATM:
Most endpoints in my application are API endpoints (since I use a frontend framework for the actual rendering), so all backend views output JSON (instead of HTML). While it is possible to send the JSON directly from the controller (this is what I am currently doing in Plug) this quickly becomes an unmaintainable mess, especially when two (or more) controllers should return the same data.
What I'd like to call
In my controller I'd like to be able to call
render/4
(in addition to the current EEx syntax) like this:(The view name could be an optional parameter which would infer it from the name of the controller. But there should be a way to explicitly specify it)
(The return value could either be a term, and Sugar would encode it automagically based on the file extension or users would have to manually encode it and return it as a
{content_type, payload}
tuple?)The big point here is that the view does no longer have access to the
conn
,Conclusion
I realize that this is trivial to implement on my own in my application, and I will most likely do so, but I believe that Sugar as a framework would benefit from seeing returned JSON also as a form of View. (Better decoupling, testability, feature parity with phoenix, ...)
This concept was heavily inspired by the way Phoenix does it, so I understand if you want so mix things up a bit though ;)
PRs welcome?