Giter Club home page Giter Club logo

armature's Introduction

Armature

Armature is an HTTP routing framework for Crystal.

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      armature:
        github: jgaskins/armature
  2. Run shards install

Usage

Armature has 2 primary components:

  • Armature::Route
    • Provides a routing graph by allowing a top-level application to delegate to various child routes based on path segments
    • Provides an ECR rendering macro to render view templates at compile time. View templates are under the views/ directory in the application root.
    • Provides a missed-match handler (r.miss) so you can provide custom 404 handling in a way that's simple and discoverable
  • Armature::Session
    • If you're using cookie-based authentication, you can use Armature sessions to persist session data between requests
    • Currently the only supported session adapter is Armature::Session::RedisStore. The value stored in the cookie is the session id and the data stored in Redis will be the session data serialized into a JSON string.
require "armature"
require "armature/redis_session"
require "redis"

class App
  include HTTP::Handler
  include Armature::Route

  def call(context)
    route context do |r, response, session|
      # The `session` can be treated as a key/value object. All JSON-friendly
      # types can be stored and retrieved. The stored session data is saved as
      # JSON and parsed into a `JSON::Any`.
      if current_user_id = session["user_id"]?.try(&.as_i?)
        current_user = UserQuery.new.find_by_id(current_user_id)
      end

      # `render` macro provided by `Armature::Route` renders the given template
      # inside the `views/` directory to the `response` object. This example
      # renders `views/app_header.ecr`.
      #
      # Note: You can render as many templates as you need, allowing for nesting
      # your UI via nested routes.
      render "app_header"
      
      # Root path (all HTTP verbs)
      r.root do
        # Execute the given block only for GET requests
        r.get { render "homepage" }

        # Execute the given block only for POST requests
        r.post do
          # ...
        end
      end

      # Delegate certain paths to other `Armature::Route`s with `on`
      r.on "products" { Products.new.call(context) }

      # Allow for authenticated-only routes
      if current_user
        r.on "notifications" { Notifications.new(current_user).call(context) }
        r.on "cart" { Cart.new(current_user).call(context) }
      end

      # Execute a block if an endpoint has not been reached yet.
      r.miss do
        response.status = :not_found
        render "not_found"
      end

      # Rendering the footer below main app content
      render "app_footer"
    end
  end
end

http = HTTP::Server.new([
  Armature::Session::RedisStore.new(
    # the HTTP cookie name to store the session id in
    key: "app_session",
    # a client for the Redis instance to store session data in
    redis: Redis::Client.from_env("REDIS_URL"),
  ),
  App.new,
])
http.listen 8080

Other useful components are:

  • Armature::Form::Helper
  • Armature::Cache
  • Armature::Component

Contributing

  1. Fork it (https://github.com/jgaskins/armature/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

armature's People

Contributors

jgaskins avatar xendk avatar

Stargazers

 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  avatar

Forkers

xendk

armature's Issues

Multiple arg types is kinda messed up

Trying to use the new multiple arg methods, I'm running into problems that might kill the multiple args idea. Take this:

      route context do |r, response, session|
        r.on "snapshot", String do |_, snapshot_id|
          snapshot = @project.snapshots[snapshot_id]

Which results in Error: expected argument #1 to 'Appocular::Storage::YAML(Appocular::Snapshot)#[]' to be String, not (Bool | String).

A bit of testing shows that the type of the block parameters is actually the union of the types returned by the called #match? methods (but only the ones that's actually called). Turns out that Tuple#map is just a macro that constructs a new tuple by inlining the block code. But the return type of self#[] would be the union of the types of the union, which in turn makes the return type of our block in on to the union of the return types of the match? methods.

Of course one can do as(Whatever) in one's on blocks, but I think it kinda ruins the DSL.

One solution that might work is making the multiple arg #is/#on into macros that unroll into nested single argument calls. That ought to make the compiler capable of pinpointing the exact type at each level, but I don't know if such a macro is possible.

What's your thoughts?

Iron out Session extensibility

Sessions are currently difficult to extend to allow for different storage backends. I'm not sure
yet what needs to be done to make that easier (or I would've done it), but it should definitely be easier. A couple things that I am sure I don't like:

  • I don't like Armature::Session::Store::Session as a constant path. Session being named twice just feels weird in a way I can't articulate.
  • The Session should not have to set specific instance variables and should ideally lean on methods for the Session::Store to call
  • The set of abstract defs for session implementations to define is almost certainly wrong
    • We should add #[]? to it because you may need to account for session keys that haven't been set yet
    • I'm not sure we should even have #[] at all, tbh — I haven't used it in years
    • Some of the methods defined in the Redis session implementation should probably be in the superclass
  • The Redis session's implementation storing an entire JSON object is probably unnecessary
    • It was originally implemented that way to batch updates to multiple session values (write once), but I'm doubtful that that's as useful as I thought it would be when I wrote it
    • We could just store each session value separately
      • The main upside here is that each session[key] = value call would immediately store that and we wouldn't have to worry about things like dirty tracking or any cleanup at all, really
      • The main downside is that different session values could expire at different times depending on the storage adapter — it would be fine with Redis since we could store all values for the same session in the same Redis key via HSET but a storage adapter like NATS KV would store each value into a different NATS KV key because it just stores strings and not more complex data structures.
    • If we stick with storing We're already using MessagePack for caching, so maybe that would've been a better idea for session storage since it's faster and uses less memory

The only concrete things I'm really concerned about are:

  • the Store is important to keep because it's used as the HTTP::Server middleware
  • the Store should be the only thing directly interacting with the backing store (Redis, NATS, Postgres, the filesystem, an in-memory hash, whatever it happens to be)
  • the Session should be simple
  • we shouldn't rely on sessions being loaded/saved for every request
    • if a wave of bots hammers your app, you don't want to be required to store sessions for all of them
    • if the request doesn't provide a cookie specifying the session and the session isn't modified in any way (for example, a form will store a csrf value), we don't store anything about the session

Capture values with blocks

One of the limitations of templates currently is that you can't pass blocks to them in templates while also capturing the result as a value:

<%== MyComponent.new do %>
  <!-- render more stuff here -->
<% end %>

This sort of pattern can be super useful when you need the component to wrap other content — that is, it may have content both before and after the block.

For example, forms need to output an opening <form> tag and an <input> with the authenticity token, then output the form's content, then output the closing </form> tag. Since the code above doesn't work, Armature::Form has to receive the response and write to it imperatively rather than letting the template engine call to_s(io). That is completely unintuitive.

One pattern that the Ruby erubi gem uses is <%|==:

<%|== MyComponent.new do %>
<%| end %>

So maybe we can do that here, as well.

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.