Skip to content
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

Remove ja_resource #864

Closed
begedin opened this issue Aug 9, 2017 · 17 comments
Closed

Remove ja_resource #864

begedin opened this issue Aug 9, 2017 · 17 comments

Comments

@begedin
Copy link
Contributor

begedin commented Aug 9, 2017

Problem

ja_resource has been giving us trouble before and will likely continue to give us more trouble.

Our initial reasoning for introducing it to our project was to reduce our controller boiler plate. However, with the way our other tools developed, this benefit has been gradually reducing and we now barely have any less boilerplate than before, and the cost has been reduced readability of code, as well as performance, in some cases.

  • we have to override several hooks per controller, due to using custom, non-basic behavior
  • it clashes heavily with our authorization layer, in some places causing us to load the same resource twice, before authorizing and performing the action
  • it makes it hard to switch to a "better" authorization framework. We've been discussing switching away from canary in the past and ja_resource was the main blocker in doing so.
  • the switch to Phoenix 1.3 means several things that further reduce the usefulness of ja_resource
    • our controllers are now in the CodeCorpsWeb namespace, while models are in CodeCorps, so each controller needs to override the ja_resource model/0 function to get it working
    • the aim is to move away from the model concept and into contexts and ja_resource depends heavily on the model concept to generally work
    • with the introduction of FallbackController and action fallbacks, we can reduce boilerplate that way
  • the library itself is not updated often, so we're bound to encounter more issues as time goes on

Conclusion

With all that in mind, my recommendation is to get rid of ja_resource completely. This would involve...

Requirements

  • rewriting our controller code to the old action system
  • make use of fallback actions and FallbackController to reduce the boilerplate that way
  • make use of our own, custom plugs to reduce boilerplate further
  • slight adjustment of our authorization plugs

Notes

The advantages are

  • less issues down the line
  • increased flexibility
  • improved code clarity and explicitness

On a personal note, it's my opinion that we introduced ja_resource early in our elixir experience when we still tried to make it work sort of like ruby. Now that I have more experience with the language and the phoenix framework, the negatives, to me, seem to outweigh the positives.

With a simple REST api, it might still work, but with the things we do here, I don't think it does.

@begedin
Copy link
Contributor Author

begedin commented Aug 9, 2017

To add, we can do this gradually. Switch one or two major controllers to it as examples, then volunteers can take it over bit by bit. Once everything is switched, we remove the package from our dependencies.

@snewcomer
Copy link
Contributor

I'd be interested in helping out here.

@begedin
Copy link
Contributor Author

begedin commented Aug 9, 2017

@snewcomer This is still at a "suggestion" level, but if we go through with it, your help is much appreciated.

@joshsmith
Copy link
Contributor

I agree with this move and think it actually decreases the potential overhead for contributors, which is always a plus in my book.

@joshsmith joshsmith changed the title Suggestion: Ditch ja_resource Remove ja_resource Aug 17, 2017
@joshsmith
Copy link
Contributor

Can we make some smaller issues to go after this, and @snewcomer would you be interested now in tackling some of those?

@snewcomer
Copy link
Contributor

@joshsmith yep I am available to help with this.

@joshsmith
Copy link
Contributor

@begedin if you have some time to break this down the way you'd like to see it, let me know.

@begedin
Copy link
Contributor Author

begedin commented Aug 18, 2017

@joshsmith I'd have to figure out how to do it first, using an example of a single controller. There's a lot of coupling going on under the hood at the moment, with our authorization being implemented around ja_resource. I'd have to take a look and try at least a small example first.

I'll see what I can do.

@begedin
Copy link
Contributor Author

begedin commented Aug 18, 2017

@joshsmith It was difficult to grasp by just looking, so I went ahead and submitted a PR using the example of the comment controller - #867

Some notes on it

  • Controllers that use ja_resource do not make it perfectly clear which actions are "enabled" for the controller and which are not
  • The change increases the amount of code per controller slightly, but the benefit is performance, and readability, IMO
  • An action at a time can be switched over, if switching the whole controller in a single PR is too large a change

@begedin
Copy link
Contributor Author

begedin commented Aug 29, 2017

#867 deals with switching one controller away. Using the knowledge gained from there, here's the full procedure on how to switch

Procedure

  • pick a controller to switch
  • view actions this controller should support in lib/code_corps_web/router.ex
  • add action_fallback CodeCorpsWeb.FallbackController. This makes it so in case a with clause within the controller action fails, a sort of "catch all" function will be called from FallbackController
  • add plug CodeCorpsWeb.Plug.DataToAttributes. This converts the data hash within the conn params from the json api structure into a flat map convenient for usage with changesets
  • for each action
    • remove that action from any load_resource, load_and_authorize_resource, etc. plugs in that controller
    • remove any def handle_#{action} functions
    • write a def #{action}(conn, params) do. For the standard create, update delete, show and index actions. See examples below on how these functions should be written, usually
    • rewrite the policy function for that action to support params instead of a changeset if necessary. See section below for pointers.

That should be the whole procedure. We already have controller tests in place, so if the process has been done correctly, the tests should pass.

Some additional steps might be necessary, however, if there are some specifics to an action. For those, we will be here to provide feedback.

How to rewrite controller actions

@spec index(Conn.t, map) :: Conn.t
def index(%Conn{} = conn, %{} = params) do
  with resources <- ResourceModule |> Query.id_filter(params) |> Repo.all do
    conn |> render("index.json-api", data: resources)
  end
end

@spec show(Conn.t, map) :: Conn.t
def show(%Conn{} = conn, %{"id" => id}) do
  with %ResourceModule{} = resource <- ResourceModule |> Repo.get(id) do
    conn |> render("show.json-api", data: resource)
  end
end

@spec create(Plug.Conn.t, map) :: Conn.t
def create(%Conn{} = conn, %{} = params) do
  with %User{} = current_user <- conn |> Guardian.Plug.current_resource,
       {:ok, :authorized} <- current_user |> Policy.authorize(:create, params),
       {:ok, %Comment{} = resource} <- %ResourceModule{} |> ResourceModule.create_changeset(params) |> Repo.insert do
    conn |> put_status(:created) |> render("show.json-api", data: resource)
  end
end

@spec update(Conn.t, map) :: Conn.t
def update(%Conn{} = conn, %{"id" => id} = params) do
  with %ResourceModule{} = resource <- ResourceModule |> Repo.get(id),
       %User{} = current_user <- conn |> Guardian.Plug.current_resource,
       {:ok, :authorized} <- current_user |> Policy.authorize(:update, resource),
       {:ok, %ResourceModule{} = resource} <- resource |> ResourceModule.changeset(params) |> Repo.update do
    conn |> render("show.json-api", data: resource)
  end
end

So to recap, the actions work like

index action

  • apply filters from query params to load data
  • render data

show action

  • load resource
  • render resource

create action

  • authorize action for current user and parameters (requires rewriting that policy to use a params map instead of a changeset, expect {:ok, :authorized}
  • perform insert action
  • render resulting resource

update and delete actions

  • load resource
  • authorize action for current user on resource, expect {:ok, :authorized}
  • perform update/delete
  • render resource, or no content for delete

Examples in code

-CodeCorpsWeb.CommentController has examples for standard index, show, create and update actions

How to rewrite policies

This consists of several relatively simple steps.

  • For each action being switch away, we need to move the def can function clause out of the defimpl block in policy.ex into the root namespace of the CodeCorps.Policy module.

  • We also need to make the clause private instead of public.

  • If any of the clauses expect a changeset, they should now expect a simple map of params:

defp can?(%User{} = user, :some_action, %Changeset{data: %Resource{}} = changeset), do: Policy.Resource.some_action?(user, changeset)

becomes

defp can?(%User{} = user, :some_action, %{} = params), do: Policy.Resource.some_action?(user, params)
  • in this case, the policy module itself also need to be rewritten to support a params map. Since in most cases, these policies simply look at the changeset.changes map, the rewrite ought to be simpler. If there is trouble, someone will be available to provide pointers.

Examples in code

  • the rewrite has been done for Policy.Comment.create?

@begedin
Copy link
Contributor Author

begedin commented Aug 29, 2017

ToDo List

Phase 1

Phase 2

  • Remove ja_resource as a dependency
  • In any remaining controllers, cleanup any ja_resource callbacks. Removing the dependency should cause warnings to appear so this should be easy
  • Remove canary as a dependency
  • Remove helpers/authentication_helpers.ex. It should no longer be necessary with canary gone.
  • Remove action_fallback CodeCorpsWeb.FallbackController from all controllers and instead move it into the CodeCorpsWeb module into the def controller quote block
  • Remove plug CodeCorpsWeb.Plug.DataToAttributes from all controllers and move it into router.ex into the :api pipeline.
  • Remove plug CodeCorpsWeb.Plug.IdsToIntegers from all controllers and move it into router.ex into the :api pipeline.

@joshsmith
Copy link
Contributor

@snewcomer thanks to @begedin there are some real specifics here for how to tackle working on this.

@snewcomer
Copy link
Contributor

Awesome! Will be tacking a few tomorrow with a coworker here!

@vishaldeepak
Copy link
Contributor

If available, id like to contribute on 1 or 2 controllers myself. Perhaps project_controller and preview_controller

@begedin
Copy link
Contributor Author

begedin commented Aug 31, 2017

@vishaldeepak, @snewcomer Feel free to do so.

Also feel free to create individual issues for those parts you would like to tackle. As long as the body of the issue contains Progress on #864, I will update the text here to mark those items as taken.

treble37 added a commit to treble37/code-corps-api that referenced this issue Oct 11, 2017
treble37 added a commit to treble37/code-corps-api that referenced this issue Oct 11, 2017
treble37 added a commit to treble37/code-corps-api that referenced this issue Oct 11, 2017
begedin pushed a commit that referenced this issue Oct 12, 2017
@begedin
Copy link
Contributor Author

begedin commented Oct 15, 2017

I've moved the remaining parts of phase 2 tasks into #1069

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants