Giter Site home page Giter Site logo

cantido / liberator Goto Github PK

View Code? Open in Web Editor NEW
34.0 3.0 1.0 558 KB

An Elixir library for building applications with HTTP

Home Page: https://hex.pm/packages/liberator/

Elixir 99.47% Earthly 0.53%
plug liberator elixir http phoenix elixir-library webmachine hacktoberfest

liberator's Introduction

Liberator

Hex.pm builds.sr.ht status liberapay goals standard-readme compliant Contributor Covenant

An Elixir library for building applications with HTTP.

Liberator is a port of the Liberator Clojure library that allows you to define a controller that adheres to the HTTP spec by providing just a few pieces of information. It implements a decision graph of simple boolean questions that lead your app to the correct HTTP status codes.

While Phoenix and Plug make routing easy, they don't do anything with content negotiation, or cache management, or existence checks, or anything like that, beyond calling the right controller function based on the HTTP method. There are a lot of decisions to make before returning the right HTTP status code, but Phoenix doesn't give you any additional power to do so. Liberator does.

Install

This package is available in Hex. Install it by adding liberator to your list of dependencies in mix.exs:

def deps do
  [
    {:liberator, "~> 1.4.0"}
  ]
end

Documentation can be generated with ExDoc and can be found online at https://hexdocs.pm/liberator.

Usage

For a basic GET endpoint, you can define an entire module in five lines of code. Technically you don't even need to implement these two, since sensible defaults are provided.

defmodule MyFirstResource do
  use Liberator.Resource

  def available_media_types(_), do: ["text/plain"]
  def handle_ok(_), do: "Hello world!"
end

It doesn't look like much, but behind the scenes, Liberator navigated a decision graph of content negotation, cache management, and existence checks before returning 200 OK. Liberator finds the best media type you support, and automatically encodes your return value. JSON is supported out of the box, and any additional types can be provided in a line of the config.

# in config.exs
config :liberator, media_types: %{
  "application/json" => Jason,
  "application/xml" => MyXmlCodec
}

# in your main body of code
defmodule MyJsonOrXmlResource do
  use Liberator.Resource

  def available_media_types(_), do: ["application/json", "application/xml"]
  def handle_ok(_), do: %{message: "hi!"}
end

A Liberator Resource implements the Plug spec, so you can forward requests to it in frameworks like Phoenix:

scope "/api", MyApp do
  pipe_through [:api]

  forward "/resources", MyFirstResource
end

Your results from questions are aggregated into the :assigns map on the conn, so you don't have to access data more than once.

defmodule MaybeExistingResource do
  use Liberator.Resource

  def exists?(conn) do
    case MyApp.Repo.get(MyApp.Post, conn.params["id"]) do
      nil -> false
      post -> %{post: post}
    end
  end
  def handle_ok(conn), do: conn.assigns[:post]
end

See more in the Getting Started guide, and in the documentation for Liberator.Resource. You can also see an example controller built with Liberator at the liberator_example project.

Maintainer

This project was developed by Rosa Richter. You can get in touch with her on Keybase.io.

Thanks

Thanks to the maintainers of the original Clojure liberator project, Philipp Meier and Malcolm Sparks, for creating such a handy tool. Their great documentation was an immense help in porting it to Elixir. And thanks to the maintainers of Erlang's webmachine for inspiring them!

Contributing

Questions and pull requests are more than welcome. I follow Elixir's tenet of bad documentation being a bug, so if anything is unclear, please file an issue or ask on the mailing list! Ideally, my answer to your question will be in an update to the docs.

Please see CONTRIBUTING.md for all the details you could ever want about helping me with this project.

Note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

License

MIT License

Copyright 2020 Rosa Richter

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

liberator's People

Contributors

cantido avatar dependabot-preview[bot] avatar dependabot[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

gitter-badger

liberator's Issues

Write a Getting Started guide

I like what I've done with the README and module docs so far, but I want a little more room to breathe. So I'll write a proper Getting Started guide, being inspired by the original Liberator's docs.

Other folks can be a huge help to me by going through the Getting Started guide I'm writing, and trying out the steps for themselves. Let me know if you run into an issue or if anything isn't clear. And make a pull request if you've got some docs you want to change.

My WIP lives at https://github.com/Cantido/liberator/blob/master/getting_started.md right now. Tell me what you think.

Allow the decision tree to be overridden

Probably just let them merge new maps into the existing map. Requiring them to override the entire thing would be a verbose pain.

This fits in with the philosophy of the library to let the developer override as much as possible. Not only does this make a great library, in my opinion, but it also lets devs work around bugs I write.

Add :trace option: :log

The :headers option is nice, but since the Elixir ecosystem is all using one logger, we can easily log the trace.

The text/plain codec should stringify maps

Is your feature request related to a problem? Please describe.
This kind of a cross between a feature and a bug. The functions later in the evaluation pipeline assume that the handlers return some sort of data structure that is then serialized. For example, handle_ok/1 should return a map, that will then be converted into a JSON string if the accepts header is application/json. If you do the same thing while the accept header is text/plain, the request crashes. Specifically, the compression step crashes, because the argument is not a binary.

Describe the solution you'd like
Stringify maps in a pretty-printed format. If you returned a map

%{a: "value a", b: %{nested: "nested value b"}`

Then the text/plain codec should return

a: value a
b:
  nested: nested value b

This almost seems like a YAML format, which TBH would be pretty nice. I might consider that.

Describe alternatives you've considered
Just calling Kernel.inspect/1 on the result. That would be ugly but it would get the job done.

Additional context
While we're in there, we should also make sure that the value returned from the media codec is a binary, so that we don't pass invalid data to the compression codecs.

For anyone who would want to help me with this change request, Liberator.MediaType.TextPlainCodec is where you'll want to be working. Make encode!/1 always return a string, or raise if there's an error.

To implement the check for a binary, add something at line 190 in Liberator.Evaluator that just checks if the return value from the codec is a binary, then throw an exception with a nice error message, and point to the module that failed.

Liberator.Codec also needs to be split into a MediaTypeCodec which is typespecced for any -> binary, then an EncodingCodec which is typespecced for binary -> binary.

Serve the Location header

Is your feature request related to a problem? Please describe.
Even though we have decision functions for moved content, and for redirects after creating content, there is no way for the user to set the Location header without just setting the header on the conn themselves.

Describe the solution you'd like
Returning the :location key in a map should be sufficient.

Describe alternatives you've considered
Letting users continue to set it themselves.

Additional context

Does not parse HTTP dates

The strftime string in the code is incorrect, so Liberator.Resource will crash if someone makes a request with a valid HTTP date.

This was copied & pasted into the tests as well, which is why it wasn't caught sooner.

Fixed by 8a8b976, tested in 068e12d.

Language codecs, AKA translator module?

Is your feature request related to a problem? Please describe.
Liberator sets the :language key in conn.assigns after some content negotiation, but its work with languages ends there. There is no help provided as far as translations are concerned. Your handler would have to check conn.assigns[:language] and branch from there, which is exactly what Liberator is designed to help avoid.

Describe the solution you'd like
Since it is content negotiation, I've considered continuing the pattern I set with encodings and media types, and to pass the result from the handlers into a translator function. My guess is that module would replace key names with localized versions, if you were dealing with maps.

Describe alternatives you've considered
Just not doing anything with languages and leaving the user on their own. That doesn't sound nice, and as mentioned earlier, the user would have to write code in a pattern that is antithetical to Liberator's philosophy.

Additional context
See at line 186 in Liberator.Evaluator how we use the media type and encoding headers to automatically format and encode the body. Create a Translator behavior, but since Liberator's default language is *, don't implement the behavior. Or, alternatively, implement a no-op behavior similar to Liberator.MediaType.TextPlainCodec for when the language is *.

Help wanted
If you have an internationalized API, tell me about some of your challenges and how you overcame them. I'd like to be a little more informed on this subject.

Getting an "OPTIONS" request does not return Options

Describe the bug
The HTTP response to an OPTIONS request does not contain the required allow header

To Reproduce
Steps to reproduce the behavior:

  1. Set up a Phoenix or Plug project
  2. Add a default Liberator.Resource, with routes to it
  3. Submit an OPTIONS request to the server

Expected behavior
The response contains an allow header containing the values returned from Liberator.Resource.allowed_methods/1

Additional context
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS

Emit tracing telemetry events

Is your feature request related to a problem? Please describe.
There's no convenient way for a running program to access its own traces.
Right now, traces were designed purely for developer consumption.

Describe the solution you'd like
Emit the complete trace log for each request as a telemetry event.

Describe alternatives you've considered
It's possible to access the trace in the conn in any of the decisions, if needed, but that feels ugly.

Additional context

Request body can't be read while obeying the Plug spec

Describe the bug
Since the return type from handlers can only be a boolean or a map, there's no way for users to call Plug.Conn.read_body/2 without just dropping the returned conn, which the Plug.Conn specs specifically say not to do.

Expected behavior
Either the raw body binary is made available in conn.assigns, or some way to return the conn is allowed for.

Additional context
Plug.Conn.read_body/2

Handling errors in actions

Hi there,

I really like the idea of liberator, but while trying it in a project of mine I came across the following issue and I do not know how to properly move forward:

When creating a new resource, I implemented the post! action, but I cannot find a nice way to handle errors happening during creation, e.g. a unique check fails that is only first recognized when Ecto tries to insert into the database.

@impl true
def post!(conn) do
    with {:ok, resource} <- MyApp.Resource.create_resource(conn.params) do
        # returning something from an action seems to be a no-op
        %{resource: resource}
    else
        %{error: %Ecto.Changeset{}} -> # how do I handle this?
    end
end

When working with plain Phoenix one usually has a FallbackController that handles Ecto changeset errors. Maybe one could allow returning values from actions, like returning the conn struct to end processing immediately. Then one could do something like this:

@impl true
def post!(conn) do
    with {:ok, resource} <- MyApp.Resource.create_resource(conn.params) do
        %{resource: resource} # merge this into the assigns to reference it in handle_created
    else
        other -> MyApp.FallbackController.call(conn, other)
    end
end

I realize that validating the parameters could be normally done in the well_formed? step, but when the error only happens on insert this cannot easily be done.
What do you think?

In any case, thanks for this great library! It's a mystery to me why it has so little attention.

An Allow header is not included when 405 Method Not Allowed is returned

Describe the bug
RFC 7231 states:

An origin server MUST generate an Allow field in a 405 (Method Not Allowed) response and MAY do so in any other response.

Liberator does not apply this header.

Expected behavior
An allow header is included in at least the response to a 405.

Additional context
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow

Related to #9, as an OPTIONS request also does not give an allow header

Remove malformed?/1 in favor of well_formed?/1

Is your feature request related to a problem? Please describe.
Since the malformed?/1 callback is a negative question, we cannot return values, like the result of a successful parse.

Describe the solution you'd like
Replace it with a well_formed?/1 decision.

Describe alternatives you've considered
Allowing some way to return a negative while also returning data. Sounds gross to me.

Additional context
Since this will remove a decision, this needs to be a major version bump.

Make an image of our own flowchart

I'm linking to the Clojure Liberator's flowchart. It would be great if we can generate a flowchart of our own from the decisions matrix in the code. If you know of a way to generate a flowchart with Elixir, let me know, and I'll code it in.

I know of a good library in Clojure, so I might extract the decision trees into EDN, parse them from the main project, but write a little Clojure utility to generate the chart. Please stop me from doing this.

Some context: The decision tree lives in Liberator.Evaluator, along with the handler and action maps. If there's a way to generate a flowchart from a map like this, I can do the work to extract that tree in a way that will make the image generation easy. Until then, there isn't really a need to extract the decision tree so I want to leave it where it is.

Need for an Example App

Nice work done so far.

I believe it will help a lot of those who want to try this out if there's an example app (may be a Todo or something) that shows proper usage of this library.

Thank you.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.