Giter Site home page Giter Site logo

hanami / controller Goto Github PK

View Code? Open in Web Editor NEW
245.0 26.0 108.0 1.58 MB

Complete, fast and testable actions for Rack and Hanami

Home Page: http://hanamirb.org

License: MIT License

Ruby 99.06% Shell 0.90% HTML 0.03%
hanami ruby rack api http controller

controller's Introduction

Hanami 🌸

The web, with simplicity.

Version

This branch contains the code for hanami 2.0.x.

Frameworks

Hanami is a full-stack Ruby web framework. It's made up of smaller, single-purpose libraries.

This repository is for the full-stack framework, which provides the glue that ties all the parts together:

These components are designed to be used independently or together in a Hanami application.

Status

Gem Version CI Depfu

Installation

Hanami supports Ruby (MRI) 3.0+

gem install hanami

Usage

hanami new bookshelf
cd bookshelf && bundle
bundle exec hanami server # visit http://localhost:2300

Please follow along with the Getting Started guide.

Donations

You can give back to Open Source, by supporting Hanami development via GitHub Sponsors.

Supporters

Contact

Community

We strive for an inclusive and helpful community. We have a Code of Conduct to handle controversial cases. In general, we expect you to be nice with other people. Our hope is for a great software and a great Community.

Contributing Open Source Helpers

  1. Fork it ( https://github.com/hanami/hanami/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

In addition to contributing code, you can help to triage issues. This can include reproducing bug reports, or asking for vital information such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to subscribe to hanami on CodeTriage.

Tests

To run all test suite:

$ bundle exec rake

To run all the unit tests:

$ bundle exec rspec spec/unit

To run all the integration tests:

$ bundle exec rspec spec/integration

To run a single test:

$ bundle exec rspec path/to/spec.rb

Development Requirements

  • Ruby >= 3.0
  • Bundler
  • Node.js (MacOS)

Versioning

Hanami uses Semantic Versioning 2.0.0

Copyright

Copyright © 2014 Hanami Team – Released under MIT License.

controller's People

Contributors

alfonsouceda avatar andrewcroome avatar arthurgeek avatar chongfun avatar cllns avatar davydovanton avatar deepj avatar depfu[bot] avatar erol avatar gustavocaso avatar jeremyf avatar jodosha avatar karlfreeman avatar lasseebert avatar lucasas avatar mereghost avatar mgrachev avatar mjbellantoni avatar narinda avatar parndt avatar russcloak avatar sidonath avatar solnic avatar stefanoverna avatar stevehodgkiss avatar tak1n avatar tercenya avatar timriley avatar vyper avatar zlw 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  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  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

controller's Issues

Session is cleared after each request

Hello! I faced an error using flash: undefined method `[]' for nil:NilClass and it looks like a bug. Further investigation showed that session is cleared after each request in the Lotus::Action::Callable#finish and thus it's no wonder that here is no "__flash" key in the session hash.

/Users/lessless/.gem/ruby/2.1.5/bundler/gems/controller-975feb8788de/lib/lotus/action/flash.rb:127:in `remove!'
/Users/lessless/.gem/ruby/2.1.5/bundler/gems/controller-975feb8788de/lib/lotus/action/flash.rb:65:in `clear'
/Users/lessless/.gem/ruby/2.1.5/bundler/gems/controller-975feb8788de/lib/lotus/action/session.rb:143:in `finish'
/Users/lessless/.gem/ruby/2.1.5/bundler/gems/controller-975feb8788de/lib/lotus/action/callable.rb:89:in `finish'

Lotus::Action::Parameters converting to hash

right now if I have something like:

class LotusParams < Lotus::Action::Params
  param :contact do
    param :email, presence: true, format: /.+@.+/
  end
end

And I call

p = LotusParams.new(contact: {email: 'xxx'})
p.to_h

I get

{"contact"=>#<#<Class:0x007fe5e222f798>:0x007fe5e219d7d0 @env={"email"=>"xxx"}, @raw=#<Lotus::Utils::Attributes:0x007fe5e219d690 @attributes={"email"=>"xxx"}>, @attributes=#<Lotus::Utils::Attributes:0x007fe5e219d4b0 @attributes={"email"=>"xxx"}>>}

I'd like to actually convert this to a raw hash of ruby primitives as specified in these params data types. I looked at the raw method, but that doesn't return the whitelist or anything, it's just the ones passed in.

Is there an easy way to convert a params object back to a validated, whitelisted raw hash?

Share behaviors between actions of the same controller

It maybe handy to share behaviors across actions belonging to the same controller.

module MyApp::Controllers
  module Articles
    include MyApp::Controller

    share do
      before { :authenticate! }
    end

    action 'Index' do
      # it will call #authenticate! before of #call
      def call(params)
      end
    end

    action 'Show' do
      # it will call #authenticate! before of #call
      def call(params)
      end
    end
end

Add a request method?

When writing code inside an Action, I miss having access to a request method that returns a Rack::Request.

Someting like this:

def request
  @request ||= Rack::Request.new(params.env)
end

I think this could be useful in general, and not just for me. Anyone have an oppinion on this?

And where should it be implemented if so? The most logical place IMO would be in Params as the Params class already knows about env.

  • Introduce a #request method which returns a Lotus::Action::Request instance
  • Add #language and #encoding to Lotus::Action::Rack.

Websocket support

I don't know if this issue should be here or in lotus repository.

It would be a good idea lotus supports websocket.

What do you think?

Implement Action#format

Look at the value of Action#content_type, and return the associated symbol.

Example:

action.content_type # => 'text/html'
action.format       # => :html

Rack has a list of the most common formats listed in Rack::Mime::MIME_TYPES.

A trivial implementation could be, but needs to be TDD'd and properly tested:

def format
  Rack::Mime::MIME_TYPES.invert[content_type].gsub(/\A\./, '').to_sym
end

The implementation should be added as a public method of Lotus::Action::Mime.

Rack middleware is not working correctly

The current implementation of Lotus::Action::Rack.use doesn't support passing the app to the middleware. To allow intercepting of requests and responses, Rack middleware is usually implemented like this:

class Middleware
  def initialize(app)
    @app = app
  end

  def call(env)
    @app.call(env)
  end
end

The current middleware implementation just passes the env to the middleware's call method and therefore doesn't work with middleware that might stop the execution of the middleware stack or modify the request or the response.

A reference implementation of Rack middleware can be found in Rack itself:

  1. https://github.com/rack/rack/blob/master/lib/rack/builder.rb#L81-L87
  2. https://github.com/rack/rack/blob/master/lib/rack/builder.rb#L144-L154

Thanks a lot!

call to Hanami::Action#call with params shouldn't modify (stringify) params

Hi,
I've worked through your getting started guide at http://hanamirb.org/guides/getting-started/ and I love the architecture of the project (especially in contrast to rails). I ran into one issue though - the example spec in 'Implementing Create action' section doesn't work any more, since the action.call(params) call stringifies params as a side-effect, thus making the following params[:book][:title] expression in the action.book.title.must_equal params[:book][:title] assertion return nil. I've traced the call path through hanami/controller and hanami/utils and this is where it happens. What I think should be done is to deep-clone the hash either there or before this call. I'm using these versions of the gems:

gem 'hanami', github: 'hanami/hanami', branch: '0.8.x'
gem 'hanami-utils', github: 'hanami/utils', branch: '0.8.x'
gem 'hanami-router', github: 'hanami/router', branch: '0.7.x'
gem 'hanami-controller', github: 'hanami/controller', branch: '0.7.x'
gem 'hanami-view', github: 'hanami/view', branch: '0.7.x'
gem 'hanami-model', github: 'hanami/model', branch: '0.7.x'
gem 'hanami-validations', github: 'hanami/validations', branch: '0.6.x'
gem 'hanami-helpers', github: 'hanami/helpers', branch: '0.4.x'
gem 'hanami-mailer', github: 'hanami/mailer', branch: '0.3.x'
gem 'hanami-assets', github: 'hanami/assets', branch: '0.3.x'

I tried to reproduce in master, but I'm having problems with connecting to the database. Looking at the master code though, I think the problem will problem will remain. For now, I think at least the example test in documentation should be updated to assert on the actual title value.

Exception from `whitelisting?` method in `params.rb`

Looks like the attributes method is not defined at all in params.rb:

Here is the stacktrace:

NameError: undefined local variable or method `attributes' for Janus::Controllers::Home::Index::Params:Class
  /Users/kir/Projects/lotus-controller/lib/lotus/action/params.rb:140:in `whitelisting?'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/params.rb:163:in `_whitelist'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/params.rb:145:in `_compute_params'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/params.rb:105:in `initialize'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/callable.rb:72:in `new'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/callable.rb:72:in `block in call'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/throwable.rb:100:in `block in _rescue'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/throwable.rb:98:in `catch'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/throwable.rb:98:in `_rescue'
  /Users/kir/Projects/lotus-controller/lib/lotus/action/callable.rb:69:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/lotus-router-0.1.1/lib/lotus/routing/endpoint.rb:66:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/http_router-0.11.1/lib/http_router.rb:193:in `process_destination_path'
  (eval):18:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/http_router-0.11.1/lib/http_router.rb:288:in `raw_call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/http_router-0.11.1/lib/http_router.rb:142:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/lotus-router-0.1.1/lib/lotus/router.rb:797:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/static.rb:119:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/builder.rb:138:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/bundler/gems/lotus-8d51feebbc85/lib/lotus/middleware.rb:57:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/bundler/gems/lotus-8d51feebbc85/lib/lotus/application.rb:152:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/session/abstract/id.rb:225:in `context'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/session/abstract/id.rb:220:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/lint.rb:49:in `_call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/lint.rb:37:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/showexceptions.rb:24:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/commonlogger.rb:33:in `call'
  /Users/kir/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/chunked.rb:43:in `call'

And the action:

module Home
  class Index
    include Janus::Action
    include Janus::Action::Session

    def call(params)
      puts params
    end
  end
end

Exception Handling Views

Currently when you set up your lotus app to handle exceptions, it returns the status code and a default page for said error is rendered by the web server (apache/nginx/etc). For production apps you definitely need custom 404/500 pages. When in doubt I often reach for Rack Rescue but it seems to have a bit of overlap with some of Lotus functionality. I'd also like to be able to serve different 404 pages depending on the app/context. Is this doable, and if not is it planned for immediate future?

Cheers

Flash session is nil when doing redirect.

Hello everyone.

I'm having an issue with flash session. I'm setting the flash session and then using a redirect_to.
In the next request I'm expecting to get the value but is returning nil.

  def call(params)
      @organization = Organization.new(params[:organization])

      if @repository.persist(@organization)
        flash[:success] = "Organization created"
        redirect_to routes.path(:manager_organizations)
      else 
        flash[:errors] = @errors = @organization.errors
      end
  end

In the view:

 - if flash[:success]
   = flash[:success]

The code is working when the flash is used in the same request. (Using the flash[:errors] in this case.)

Inheritance bug

I'm using lotus-controller 0.2.0

A standalone controller/action is returning a valid rack response:

require 'lotus/controller'
module MyApp
  class MyAction
    include Lotus::Action
    def call(params)
      self.body = 'hello'
    end
  end
end

# MyApp::MyAction.call({}) => [200, {"Content-Type"=>"application/octet-stream"}, ["hello"]]

However, if I inherit from a base class, an invalid response is being generated

require 'lotus/controller'
module MyApp
  class BaseAction
    include Lotus::Action
  end
end

module MyApp
  class MyAction < BaseAction
    def call(params)
      self.body = 'hello'
    end
  end
end
# MyApp::MyAction.call({}) => "hello"

Bug?

My original intention is to add a few simple authentication before filters to the base class so that they can be inherited by all subclasses.. but it doesn't look like inheritance is working.

Lotus::Action should not dump the exception if the exception is handled

Given the following action

class Show
  include Api::V2::Action

  handle_exception RecordNotFound => :handle_record_not_found

  def call(params)
    # ...
  end

  def handle_record_not_found(exception)
    halt(404, { message: exception.message }.to_json)
  end
end

An HTTP request for an invalid resource will correctly render the error message

{"message":"Domain `3999' not found"}

However, the exception is also dumped in the log

ActiveRecord::RecordNotFound: Domain `3999' not found
  ...
  ...
  ...
  /Users/weppos/.rvm/gems/ruby-2.1.5@dnsimple/gems/lotus-controller-0.3.1/lib/lotus/action/callbacks.rb:149:in `call'
  /Users/weppos/.rvm/gems/ruby-2.1.5@dnsimple/gems/lotus-controller-0.3.1/lib/lotus/action/callable.rb:73:in `block in call'
  /Users/weppos/.rvm/gems/ruby-2.1.5@dnsimple/gems/lotus-controller-0.3.1/lib/lotus/action/throwable.rb:104:in `block in _rescue'
  /Users/weppos/.rvm/gems/ruby-2.1.5@dnsimple/gems/lotus-controller-0.3.1/lib/lotus/action/throwable.rb:102:in `catch'
  /Users/weppos/.rvm/gems/ruby-2.1.5@dnsimple/gems/lotus-controller-0.3.1/lib/lotus/action/throwable.rb:102:in `_rescue'

making it hard to distinguish handled vs not-handled exceptions. That's because _rescue always dumps the error, no matter if it was handled or not.

      def _rescue
        catch :halt do
          begin
            yield
          rescue => exception
            _reference_in_rack_errors(exception)
            _handle_exception(exception)
          end
        end
      end

My proposal is to dump the exception only if not handled. Though?

Tempfile possibly being stringified during param whitelisting

Hallo,

Congratulations on the new release!

While upgrading to 0.8 a file upload feature started to break. On looking more closely, it seemed that when a multi part file upload is made from a form and then checked with a dry-v validation, the value of of a file parameter is stringified.

https://github.com/hanami/controller/blob/master/lib/hanami/action/params.rb#L129

This is the example we have been trying out:

https://github.com/hanami/controller#validations--coercions

I created a little app to see what kind of types were being delivered in the params with and without validations. It seems without validations the tempfile is not being modified.

https://github.com/matthewling/test-file-upload-hanami

Thanks for your help!

Matthew

Cookies: automatic "expires" setting

Old browsers (IE 6, 7, 8), don't support max-age setting.

If we set default :max_age from Lotus::Configuration, or we specify inline in a cookie cookie[:foo] = {value: 'bar', max_age: 300}, it will be ignored.

I think it's our role to provide transparent solutions to browsers quirks.
We should set expires automatically, when it isn't present, but max-age is.

# Do nothing
cookies[:foo] = 'bar'

# Do nothing
cookies[:foo] = { value: 'bar', expires: Time.parse('2015-05-14 12:01:00') }

# Automatically set expires 
cookies[:foo] = { value: 'bar', max_age: 120 }

See http://mrcoles.com/blog/cookies-max-age-vs-expires/

Errors on redirect

Hi,

I've been trying to get the errors of the previous action after a redirect (which I've seen it's possible according to lotus/controller code) but I can't. I've also been searching for examples on how to do this right but I found nothing.

I have this on my controller config:

cookies true
sessions :cookie, secret: "foo"

On my action after the redirect I have this:

def call(params)
  puts errors.inspect
end

The first time I submit my form everything is ok, I get the errors on the action after the redirect. But if I submit the form more times I get the same errors as the first time no matter what the input contents are.

I've also noticed that two cookies "rack.session" are set. One with the path "/" and one with the path "/foo" where /foo is the endpoint of the form action. Maybe it has something to do with that.

Format for IE8

Hi
I posted this in the hanami chat posting here for collecting all information;

I was wondering if someone can give me a little pointer here as I’m not familiar with how the self.format is being selected.

My app works fine in most browsers and gets the format :html matching text/html from the HTTP_ACCEPT except in IE8.

It keeps requesting :jpeg with the error

Hanami::View:MissingTemplateError: Can’t find template ’signup/new’ for ‘jpeg’ format.

Since IE8 has */* at the end of its HTTP_ACCEPT should it not also get :html?

I saw the issue #104 raised for wrong IE format and the fix #105
This was added to version 0.4.0, I have version 0.6.1 running.

I tried to debug best_q_match in lib/lotus/action/mime.rbit seems like */* is being resolved as application/vnd.lotus-1-2-3, since it is the first in the list of available mime types.

MIME types restriction not working properly

According to the documentation, if we define a controller and specify which MIME type we want to accept, the app should respond with a client error when it receive requests with a not 'acceptable' MIME type.

class Show
  include Lotus::Action
  accept :html, :json

  def call(params)
    # ...
  end
end

# When called with "\*/\*"            => 200
# When called with "text/html"        => 200
# When called with "application/json" => 200
# When called with "application/xml"  => 406

But unfortunately this is not working in the current version. At this moment, even if you specify a content-type, when the app receives requests with other content-types it process the action normally.

#call method is called even when a before callback does a redirect_to.

Hi everyone!

I'm experimenting some random behaviors.

I have a before :authenticate! callback that redirect to '/login' when a session doesn't exist. Simple.

When making the tests for the action I wanted to check that it returns 302 when user isn't authenticated.

When running the tests, the tests were running the #call method and calling the Repository when it should stop the chain and redirect.

Here some examples.

module Web::Controllers::Posts
  class Index
    include Web::Action
    # include Web::AuthenticatedAction

    before :authenticate!

    def call(params)
      puts  "This should not be called"
    end

    def authenticate!
      puts "Redirect should stop here"
      redirect_to '/login'
    end
  end
end

Here is the output when I request the controller route

[2015-03-09 18:04:15] INFO  WEBrick 1.3.1
[2015-03-09 18:04:15] INFO  ruby 2.1.4 (2014-10-27) [x86_64-darwin14.0]
[2015-03-09 18:04:15] INFO  WEBrick::HTTPServer#start: pid=49653 port=2300
Redirect should stop here
This should not be called
::1 - - [09/Mar/2015:18:04:18 -0400] "GET / HTTP/1.1" 302 - 0.9164
::1 - - [09/Mar/2015:18:04:19 -0400] "GET /login HTTP/1.1" 200 - 0.8267

The same apply for tests.

Ability to access unfiltered request parameters

This is the result of a chat I had with @jodosha about a real-world scenario.

Scenario

Let's suppose I configure an action with some param whitelisting.

class Signup
  include Lotus::Action

  params do
    param :first_name
    param :last_name
    param :email
  end

  def call(params)
    # ...
  end
end

Once the action is triggered, the params object will return a filtered list of params. Therefore, if the action is called with

POST /signup?first_name=Simone&last_name=Carletti&email=whatever&foo=bar

the action will strip the unallowed foo parameter. In most cases, this is what we want. However, let's assume that I have a command (similar to the Interactor @jodosha's idea) that I use to process the signup, that takes as input a whitelisted Hash of signup attributes.

class Signup
  include Lotus::Action

  params do
    param :first_name
    param :last_name
    param :email
  end

  def call(params)
    SignupInteractor.execute(params)
  end
end

So far so good. But our use case is a little bit more complex. We want to allow people to signup, and being redirected to a specific return_to path passed in query string, or a default url if the return path is not present.

class Signup
  include Lotus::Action

  params do
    param :first_name
    param :last_name
    param :email
  end

  def call(params)
    SignupInteractor.execute(params)

    redirect_to params[:return_to] ? params[:return_to] : 'default'
  end
end

Here we have a problem. I don't have access anymore to the return_to. There are a number of possible workarounds, however both @jodosha and I agree that there should be a way to access the unfiltered list of params in a request.

This is just a possible use case, but there are a lot of them.

API

This is a call for feedback. Some options are:

  • params.raw[:whatever]
  • params.original[:whatever]

Proposals?

Make parameter types required

For example, param :id, Integer

Coercing user input into an expected type is a good practice. Without specifying a type, based on what the parser in rack gives you, you could end up with a string, array, hash, nil or a tempfile. It would be great if lotus made developers think about this and provided guarantees that the parameter would be either the given type or nil.

Rails has had vulnerabilities in the past that would have been non-issues if the input was coerced into an expected type before passing it along to ActiveRecord [1].

Thoughts?

[1] https://groups.google.com/forum/?fromgroups=#!topic/rubyonrails-security/t1WFuuQyavI

WebSocket

WebSocket support! 💖

Example:

module Chat
  class Show
    include Lotus::Action
    include Lotus::Action::WebSocket

    def call(params)
      websocket.on_open do
        send_data "Hi"
      end

      websocket.on_message do |message|
        send_data "Echo: #{ message }"
      end
    end
  end
end

The corresponding client side code:

document.addEventListener("DOMContentLoaded", function(event) { 
  var socket       = new WebSocket("ws://localhost:2300/chat");

  socket.onmessage = function(message) {
    console.log(message);
    window.alert(message.data);
  };

  socket.send("Hello");
}

Default headers

Allow the configuration of default headers. All the response will send those default values.
Developers MUST have the chance to override one or more headers in each single action.

Default headers are expressed as a Hash, where keys are the header name and values the header value. The default value SHOULD be an empty Hash.

Lotus::Controller.configure do
  default_headers({
    'X-Foo' => 'bar',
    'Content-Security-Policy' => '...'
  })
end

Params validations

Right now, the params passed to #call are an instance of Lotus::Action::Params which is a nice wrapper around a Ruby Hash.

I would love to pass subclasses of Params which are aware of the specific use case and they should be able to do:

  • White label filtering: only the declared params will be available in the final object passed to #call.
  • Coercions (optional feature): coerce a single param to the given Ruby class.
  • Validations: if the given params aren't valid, halt with an HTTP status.

This approach has the following advantages:

  • Avoid the need for low level entities to have mass assignment protection features.
  • Save CPU cycles in case of validation failures: there is no need to hit lower layers if the data isn't valid.

We probably need to add this interface:

class Lotus::Action::Params
  def self.param(name, type, options = {})
    # ...
  end

  def initialize(params)
    @params = params
  end

  def validate!
    halt 412 unless valid?
  end

  def valid?
    # ...
  end
end

And implement one of those solutions:

A

class Signup
  include Lotus::Action
  params SignupParams

  def call(params)
    puts params.class # => SignupParams
  end
end

class SignupParams < Lotus::Action::Params
  param :first_name, String, presence: true
  param :last_name,  String, presence: true
  param :email,      String, presence: true, format: /(.*)/
end

Where:

def self.params(params_class)
  before do |params|
    params_class.new(params).validate!
  end
end

B

class Signup
  include Lotus::Action

  params do
    param :first_name, String, presence: true
    param :last_name,  String, presence: true
    param :email,      String, presence: true, format: /(.*)/
  end

  def call(params)
    puts params.class # => Signup::Params
  end
end

Where .params would generate an inner class of the action Signup::Params, which inherits from Lotus::Action::Params.

Exceptions raised from callbacks aren't handled by handle_exceptions

Example:

module Projects
  class Show
    include Hanami::Action

    configuration.handle_exceptions true
    handle_exception ActiveRecord::RecordNotFound => 404

    before :find_account!

    def call(params)
      @account.projects.find(params[:id])
    end

    private

    def find_account!
      @account = Account.find(params[:account_id])
    end
  end
end

When this action is called, instead of handle the ActiveRecord::RecordNotFound exception as a 404, it blows up. Probably this happens because callbacks are executed outside of the _rescue block, which is responsible of exceptions handling.

Framework freeze

The Lotus philosophy for application boot is: load all the code at the beginning, then freeze the parts that may suffer of accidental changes and lead to software defects.

This MUST include:

  • Lotus::Controller.configuration

    All the configurations of the single controllers (Not required anymore because of #49)

    All the configurations of the single actions (Postponed for now)

This process should be triggered by a single method.

Wrong format on Internet Explorer

Howdy fellow lotus lovers!

I noticed that two of my Lotus Production apps won't allow Internet Explorer access. They would immediately throw a 500 error when trying to access them. After some digging, these are my findings:

The HTTP_ACCEPT header that IE (short for Internet Explorer from now on) is coming with; is text/html, application/xhtml+xml, image/jxr, */* when checking self.format inside an controller action we can see that this translates to :'123'. Weird right?

After digging around in lotus/controller I noticed this particular method inside mime.rb

def accepts
  unless accept == DEFAULT_ACCEPT
    ::Rack::Utils.best_q_match(accept, ::Rack::Mime::MIME_TYPES.values)
  end
end

Since accept is equal to HTTP_HEADER the result from Rack's best_q_match is "application/vnd.lotus-1-2-3" (more info on this here) which is what is ultimatetly determining the not-so-cool :'123' format.

SIDENOTE

I took a few side steps onto Rack to check out this function and this is the result from q_values for this particular string:

[["text/html", 1.0], ["application/xhtml+xml", 1.0], ["image/jxr", 1.0], ["*/*", 1.0]]

Seems weird that the quality of all of them is 1.0 considering that text/html should be the one to be respected, in particular */* is the one being extrapolated to "application/vnd.lotus-1-2-3", I know that our very own @jodosha patched this function and added a test case that involves this particular string so I believe something broke in Rack along the way. You can take a look at @jodosha's patch here

I want to help out here, so my question is, is this something that should be patched/fixed in Lotus or completely delegate this to Rack? It's believe is quite bad for us (Lotus) that IE is broken out of the box.

Lotus::Action::Params should deep symbolise keys of itself

I posted following nested params to a controller action:

"user" => { "first_name" => "Sean", "last_name" => "Bean" }

The controller has access to params which is instance of Lotus::Utils::Attributes. It works fine with 1 level hash, but with nested level hash, it turns out to be not the case. Let's see this example:

params['user'] #=>  { "first_name" => "Sean", "last_name" => "Bean" }
params[:user] #=>  { "first_name" => "Sean", "last_name" => "Bean" }
# => wot? I would expect { first_name: "Sean", last_name: "Bean" }

UPDATE: it seems to be this is an issue of Lotus::Utils::Attributes, which is lotus/utils. But I want to bring up a discussion here whether we should provide indifferent access to our params hash or not. I don't find indifferent access hash has any particular merits aside of convenient way to access hash. I'd vote for a string hash.

:handle_exception doesn't work consistently for Action and Controller

Given the following configuration

Lotus::Controller.configure do
  handle_exception ActiveRecord::RecordNotFound => :handle_record_not_found
end

and the following handler

def handle_record_not_found(exception)
  halt(404, { message: exception.message }.to_json)
end

If I define the handler in the action, the code is working as expected. If I define the action in the configure block

Lotus::Controller.configure do
  handle_exception ActiveRecord::RecordNotFound => :handle_record_not_found

  def handle_record_not_found(exception)
    halt(404, { message: exception.message }.to_json)
  end
end

the code crashes with error

Puma caught this error: undefined method `to_i' for :handle_record_not_found:Symbol (NoMethodError)

Is this expected? Is there any different between the error handling in the action, and in the configuration?

[Breaking] Remove Lotus::Controller DSL

We should deprecate the following API:

class HomeController
  include Lotus::Controller

  action 'Index' do
    # ...
  end
end

In favor of:

module Controllers
  module Home
    class Index
      include Lotus::Action
     end
   end
end

The reasons are:

  • It's not object oriented
  • It's magic ✨ 😉
  • Why use a DSL where vanilla Ruby is enough?
  • It isn't clear what's generated from that API: class or module?
  • It encourages controllers to be classes [1].
  • It encourages single file controllers.

[1]: We have a convention in Lotus full stack application, that wants actions under a certain namespace: <Application>::Controllers::<Controller>::<Action>. Example: Bookshelf::Controllers::Books::Index. The natural way to have namespaces in Ruby is via modules, for this reason, I want controllers to be modules too, not classes.

Exposures persist between requests?

I noticed that exposures doesn't get reset between requests. If the action object is the same between requests, then exposures being cached via ||= will persist between requests as well. In my app, I had to set @exposures = nil so that the next time I called exposures, it would reload the current state. An example:

class Show
  include Lotus::Action
  expose :thing
  def call(params)
    @thing = params[:asdf]
  end
end
show = Show.new
show.call({asdf: 1})
show.exposures.fetch(:thing) #=> 1
show.call({asdf: 'two'})
show.exposures.fetch(:thing) #=> 1

class MyShow
  include Lotus::Action
  expose :thing
  def call(params)
    @exposures = nil # Only difference
    @thing = params[:asdf]
  end
end
my_show = MyShow.new
my_show.call({asdf: 1})
my_show.exposures.fetch(:thing) #=> 1
my_show.call({asdf: 'two'})
my_show.exposures.fetch(:thing) #=> 'two'

Is this expected behavior?

Lotus::Controller and unicorn - Action status codes persist to next HTTP request.

Our app uses Lotus::Controller and Lotus::Routes, and our production setup uses unicorn.

What we're finding is that if one of the unicorn workers runs an action that explicitly sets a status code ( 500 in our case ) all subsequent HTTP requests that go through the same action and same worker, which do not set a status code, finish with the previously set 500.

Our workaround is to explicitly set status codes in our actions, including 200. However I would have expected that the default, when status code not set, would be 200 however, and the lotus docs seem to suggest that, there are plenty of examples that do not set a status 200.

Lotus::Utils::Hash conflicts with ActiveSupport::HashWithIndifferentAccess.new

First of all, let me say that I'm reporting the issue as it is (since it took me a while to understand what was going on) and it may not necessarily be an issue on your side. However, I'd like to start the discussion to figure out what is the best approach here.

I have an object, which performs some Hash manipulation. The input is a controller parameter, and since I'm using both Rails and Lotus, the object can be either a Lotus Params instance or a Hash (in case of Rails).

To avoid incompatibilities, the initializer calls to_h on the input, to make sure to work with a simple hash rather subclasses or specific implementations.

class ContactParams
  def initialize(params)
    @params = params.to_h
  end

  def to_h
    @params.slice(...)
  end
end

To make sure I can always reference the Hash with a symbol, but to bypass the symbol GC issue in Rails < 2.2 I'm using the ActiveSupport::HashWithIndifferentAccess.new class.

class ContactParams
  def initialize(params)
    @params = ActiveSupport::HashWithIndifferentAccess.new(params.to_h)
  end
end

Here comes the problem. Have a look at this console log

(byebug)  ActiveSupport::HashWithIndifferentAccess.new(Lotus::Utils::Hash.new({ foo: "bar" }))
{}
(byebug) ActiveSupport::HashWithIndifferentAccess.new({ foo: "bar" })
{"foo"=>"bar"}

Because params.to_h returns an instance of Lotus::Utils::Hash and not Hash, when the instance is passed to HashWithIndifferentAccess, for some reason the resulting object is {} (the values are wiped out).

As I mentioned, I still need to debug at which level the values are lost and who is the guilty.

Exception notifiers compatibility

Lotus::Controller dumps exceptions in rack.errors. This is specified by Rack SPEC.

rack.errors | See below, the error stream.


The Error Stream

The error stream must respond to puts, write and flush.

puts must be called with a single argument that responds to to_s.
write must be called with a single argument that is a String.
flush must be called without arguments and must be called in order to make the error appear for sure.
close must never be called on the error stream.


On the other hand, the most popular exception notifier services use rack.exception. This isn't documented by Rack SPEC, but appears to be a de-facto standard for these services. Here's the code reference for their Ruby clients:


I can't find the reason why this is used.

Flash in unsable

Hello! There is a problem with empty flash

NoMethodError: undefined method `[]' for nil:NilClass
    /Users/lessless/.gem/ruby/2.1.4/bundler/gems/controller-534fc0efe543/lib/lotus/action/flash.rb:103:in `data'
    /Users/lessless/.gem/ruby/2.1.4/bundler/gems/controller-534fc0efe543/lib/lotus/action/flash.rb:50:in `[]'
    /Users/lessless/tmp/lotusession/app/templates/application.html.erb:7:in `block (2 levels) in singleton class'
    /Users/lessless/tmp/lotusession/app/templates/application.html.erb:6:in `each'
    /Users/lessless/tmp/lotusession/app/templates/application.html.erb:6:in `block in singleton class'
    /Users/lessless/tmp/lotusession/app/templates/application.html.erb:-6:in `instance_eval'
    /Users/lessless/tmp/lotusession/app/templates/application.html.erb:-6:in `singleton class'
    /Users/lessless/tmp/lotusession/app/templates/application.html.erb:-8:in `__tilt_70348439917320'
require "lotus"

module App
  class Application < Lotus::Application
    configure do
      layout :application
      routes do
        get '/', to:      'home#index'
        get '/login', to: 'login#index'
      end
      sessions :cookie, secret: 'zzzzzzzzzzzzzzzzzzzzzz'
      controller.prepare do
        expose :flash
      end
      view.root Dir.pwd
    end

    load!
  end

  module Controllers::Home
    include Controller
    class Index
      include Action
      def call(params)
        # uncomment this line and everything will work
        # flash[:error] = "OMG"
      end
    end
  end

  module Controllers::Login
    include Controller
    class Index
      include Action

      def call(params)
        puts session.inspect
      end
    end
  end

  module App::Views::Home
    class Index
      include App::View
    end
  end

  module App::Views::Login
    class Index
      include App::View
    end
  end

  class App::Views::ApplicationLayout
    include Lotus::Layout
  end

end

run App::Application.new

here is the template

<!-- app/templates/application.html.erb --> 
<html>
  <head>
  </head>

    <div class="flash-messages">
      <% %i(error notice success).each do |type| %>
        <% if flash[type]  %>
          <div class="flash-message-<%= type %>"> <%= flash[type] %> </div>
        <% end %>
       <% end %>
    </div>


    <%= yield %>

  </body>
</html>

Incorrect Mime Type with MRI 2.2.0-preview1 on Ubuntu

For now it's only verified with Rack <= 1.5, where we apply lib/rack-patch.rb.

# input
"text/html,application/xhtml+xml,application/xml;q=0.9"

# weighted input
[["text/html", 1.0], ["application/xhtml+xml", 1.0], ["application/xml", 0.9], ["application/vnd.lotus-1-2-3", 0.8]]

# expected intermediary computation
[["application/vnd.lotus-1-2-3", 0.8], ["application/xml", 0.9], ["application/xhtml+xml", 1.0], ["text/html", 1.0]]

# actual intermediary computation on the affected platform
[["application/vnd.lotus-1-2-3", 0.8], ["application/xml", 0.9], ["text/html", 1.0], ["application/xhtml+xml", 1.0]]

Current hypotesis is about a bug of Array#sort_by.

EDIT:

# array_sort_by.rb
input = [["text/html", 1.0], ["application/xhtml+xml", 1.0], ["application/xml", 0.9], ["application/vnd.lotus-1-2-3", 0.8]]
output = input.sort_by do |_, weight|
  weight
end

puts output.inspect
$ ruby -v
ruby 2.0.0p451 (2014-02-24) [x86_64-linux]
$ ruby array_sort_by.rb
[["application/vnd.lotus-1-2-3", 0.8], ["application/xml", 0.9], ["application/xhtml+xml", 1.0], ["text/html", 1.0]]

$ ruby -v
ruby 2.2.0preview1 (2014-09-17 trunk 47616) [x86_64-linux]
$ ruby array_sort_by.rb
[["application/vnd.lotus-1-2-3", 0.8], ["application/xml", 0.9], ["text/html", 1.0], ["application/xhtml+xml", 1.0]]

$ ruby -v
ruby 2.2.0preview2 (2014-11-28 trunk 48628) [x86_64-linux]
$ ruby array_sort_by.rb
[["application/vnd.lotus-1-2-3", 0.8], ["application/xml", 0.9], ["text/html", 1.0], ["application/xhtml+xml", 1.0]]

Ability to configure a filter in a specific position

Granted that one of @jodosha's goal was to keep callbacks as simple as possible, I have a case that I feel reveal a reasonable improvement.

Let's assume I have a filter called authenticate! that must be run on every action.

class Show
  before :authenticate!

  def call(params)
  end
end

On a very few actions (that's one of the key conditions), let's say 10 out of 100, I need a specific validation to happen before the authenticate.

class Index
  before :validate!, :authenticate!

  def call(params)
  end
end

I can indeed stick with the explicit example above, but that would mean I can't extract the before :authenticate! into a controller config, or a module.

Therefore, I wonder what do you think about providing a finer way to control where I want to add my filter. Right now, there's only a Chain#add method.

I propose to add something similar to

  • Chain#append(callback)
  • Chain#prepend(callback)
  • Chain#insert(before, callback) (or Chain#insert_before(before, callback) and Chain#insert_after(after, callback))

I can work on a patch, if we agree on naming and API.

Raise exception for reserved words (namely `flash`)

This issue started as a request for documentation that flash is a reserved word when using Hanami::Action::Session: hanami/hanami#595

Instead of just documenting it, we should help the developer by raising an exception when they try to use any reserved word.

One reserved word is flash but we should make sure all reserved words are covered.

(Thanks for the original issue @rafaels88)

Code reloading

Make Lotus::Controller compatible with Rack::Reloader.
The implementation should take care of Lotus::Controller::Configuration internals.

Built In Authentication

This may not be the proper channel for this discussion. If not, please let me know and I'll make sure to post accordingly in the future.

I was curious if there was room for discussion, or had already been any discussion, about integrating an authentication system into Hanami. I've seen this in the PHP framework Laravel and it is always something that has interested me. Providing a solution for authentication out of the box. This is a problem solved in Rails by using Devise. Devise is great, but yet another dependency.

I know that being lightweight is one of Hanami's strong points, so I'm not sure if this would be too much, but I'd love to talk more about it. If not, no worries 😄

Routes and helpers - how to get them as objects

Hey, working on a nice example project with Hanami and Trailblazer. It's great fun. ❤️ I have two things, though, I'd love to discuss when using Cells with Hanami.

Cells in Hanami

We use Cells as a replacement for Hanami::View, we simply render the cell in the `Action.

class Create
  include Hanami::Action

  def call(params)
    # whatever
    self.body = Some::Cell.(bla, routes: routes).() # note the :routes injection.
  end
end

As you can see, the cell's returned markup is assigned via Action#body= and done. It works great so far.

Route object

In the cell, you might want to use Hanami's route helpers. This I do by injecting routes from the controller into the cell (see above), then in the cell or its view I call something along routes.sheets_path and it works.

My first question now is: Can we access the routes object without having to have a controller? This is especially helpful in tests, where we currently need a controller to get a route object.

it do
  controller = My::Controller.new

  html = Some::Cell.(bla, 
    routes: controller.routes).() # can we do without controller somehow here?
end

Asset Helpers

The other problem is: how can we access the asset helpers such as javascript_tag without including a helper module into the cell? I would love to have a helpers object here, so the cell doesn't get polluted with Hanami helpers.

Currently, I have the following hack.

class Create
  include Hanami::Action
  includer.send :include, Hanami::Assets::Helpers # this might not be how it's intended.

  def call(params)
    # whatever
    self.body = Some::Cell.(bla, routes: routes, helpers: self).() # note :helers injection.
  end
end

With this hack, in the cell, we can do helpers.javascript_tag and it works out nicely.

Again, in the test, this requires a controller.

What we need

My goal is to have objects that provide helper methods as opposed to mixing them into the cell "The Rails Way™". Even if I include the Assets::Helpers into the cell, it complains about a missing configuration.

This must be because of Hanami's global configuration system which I really do not wanna touch with Cells.

Any ideas how we could provide those two objects without a controller? 🍻

Validating nested params

Hey,

It doesn't seem to be possible to validate nested params at the moment. I was hoping this syntax would work:

params do
  param :job do
    param :title, presence: true
  end
end

with session + cookie enabled, set-cookie header for rack.session is sent twice

When having cookies and sessions enabled, then the set-cookie header for rack.session is sent twice, once with and once without path.
This leads to confusing state when modifying session content.

Steps to reproduce:

  1. create a new lotus app
  2. modify files as follows
# application.rb: enable cookies AND sessions
...
cookies true
sessions :cookie, secret: 'verrrrrry-secret'
..

# routes.rb
get '/', to: 'home#index'

# web/controllers/home/index.rb
module Web::Controllers::Home

  class Index
    include Web::Action

    def call(params)
      self.body = 'Welcome'
    end
  end
end
  1. lotus server
  2. visit localhost:2300 and check response headers, you'll see
Set-Cookie:rack.session=BLA...BLA; HttpOnly
Set-Cookie:rack.session=BLA...BLA; path=/; HttpOnly

First cookie is from cookies second one from sessions

Problem can be "fixed" by setting the path of cookies to the same value as the path of sessions but still two headers with same content are sent back to the browser.

Whitelisting should occur even if no params are defined

Currently if I don't call params in an action the whitelisting won't be applied. The intention is clearly read in https://github.com/lotus/controller/blob/master/lib/lotus/action/params.rb#L112-L114 which makes me think this is not accidental.

However if Lotus promotes being explicit about things I'd suggest that whitelisting occurs at all times because it's weird that I can read anything if I don't have any params, but can't read anything extra once a param is defined.

So params[:auth_token] is visible if there are no defined params but when I define param :amount, presence: true the code that used to work won't work. This is surprising as opposed to the first case never working and asking me to be explicit about allowing that attribute.

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.