xenon 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/.codeclimate.yml +18 -0
 - data/.gitignore +2 -0
 - data/.travis.yml +6 -0
 - data/Gemfile +10 -0
 - data/Guardfile +0 -32
 - data/README.md +59 -5
 - data/examples/hello_world/config.ru +3 -0
 - data/examples/hello_world/hello_world.rb +17 -0
 - data/lib/xenon.rb +62 -49
 - data/lib/xenon/auth.rb +63 -0
 - data/lib/xenon/errors.rb +1 -0
 - data/lib/xenon/etag.rb +48 -0
 - data/lib/xenon/headers.rb +2 -4
 - data/lib/xenon/headers/accept.rb +3 -2
 - data/lib/xenon/headers/accept_charset.rb +5 -5
 - data/lib/xenon/headers/accept_encoding.rb +5 -5
 - data/lib/xenon/headers/accept_language.rb +5 -5
 - data/lib/xenon/headers/authorization.rb +7 -53
 - data/lib/xenon/headers/cache_control.rb +3 -3
 - data/lib/xenon/headers/content_type.rb +1 -1
 - data/lib/xenon/headers/if_match.rb +53 -0
 - data/lib/xenon/headers/if_modified_since.rb +22 -0
 - data/lib/xenon/headers/if_none_match.rb +53 -0
 - data/lib/xenon/headers/if_range.rb +45 -0
 - data/lib/xenon/headers/if_unmodified_since.rb +22 -0
 - data/lib/xenon/headers/user_agent.rb +65 -0
 - data/lib/xenon/headers/www_authenticate.rb +70 -0
 - data/lib/xenon/media_type.rb +24 -2
 - data/lib/xenon/parsers/basic_rules.rb +38 -7
 - data/lib/xenon/parsers/header_rules.rb +49 -3
 - data/lib/xenon/parsers/media_type.rb +4 -3
 - data/lib/xenon/quoted_string.rb +11 -1
 - data/lib/xenon/routing/directives.rb +14 -0
 - data/lib/xenon/routing/header_directives.rb +32 -0
 - data/lib/xenon/routing/method_directives.rb +26 -0
 - data/lib/xenon/routing/param_directives.rb +22 -0
 - data/lib/xenon/routing/path_directives.rb +37 -0
 - data/lib/xenon/routing/route_directives.rb +51 -0
 - data/lib/xenon/routing/security_directives.rb +20 -0
 - data/lib/xenon/version.rb +1 -1
 - data/spec/spec_helper.rb +3 -0
 - data/spec/xenon/etag_spec.rb +19 -0
 - data/spec/xenon/headers/if_match_spec.rb +73 -0
 - data/spec/xenon/headers/if_modified_since_spec.rb +19 -0
 - data/spec/xenon/headers/if_none_match_spec.rb +79 -0
 - data/spec/xenon/headers/if_range_spec.rb +45 -0
 - data/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
 - data/spec/xenon/headers/user_agent_spec.rb +67 -0
 - data/spec/xenon/headers/www_authenticate_spec.rb +43 -0
 - data/xenon.gemspec +4 -3
 - metadata +60 -10
 - data/lib/xenon/routing.rb +0 -133
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA1:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: c82b7b50f76ea05e462ab4969825863e27e1f51a
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 033692599c1325d96064ee10d72ff2302b51db5f
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 55964419254b71d4c37032fc6c51fb0bda9ac770b04353b0b0f6e79428b372ce723504a33b0331380440398586ba1f647171e142188d34557e144b8632409689
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 1ff9d197b45558a4dc20ad0f5dbd8ce037ea4301384149d66ebc29d80787c05f3b4be3b5dde9ea9e2eb110af18af0834423a9edc0f3f04cbd4d0dc3d89bae391
         
     | 
    
        data/.codeclimate.yml
    ADDED
    
    | 
         @@ -0,0 +1,18 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # For more details, see here:
         
     | 
| 
      
 2 
     | 
    
         
            +
            # http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            # For a list of all available engines, see here:
         
     | 
| 
      
 5 
     | 
    
         
            +
            # http://docs.codeclimate.com/article/296-engines-available-engines
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            engines:
         
     | 
| 
      
 8 
     | 
    
         
            +
              rubocop:
         
     | 
| 
      
 9 
     | 
    
         
            +
                enabled: true
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            ratings:
         
     | 
| 
      
 12 
     | 
    
         
            +
              paths:
         
     | 
| 
      
 13 
     | 
    
         
            +
                - lib/**/*
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            exclude_paths:
         
     | 
| 
      
 16 
     | 
    
         
            +
              - examples/**/*
         
     | 
| 
      
 17 
     | 
    
         
            +
              - spec/**/*
         
     | 
| 
      
 18 
     | 
    
         
            +
              - vendor/**/*
         
     | 
    
        data/.gitignore
    CHANGED
    
    
    
        data/.travis.yml
    ADDED
    
    
    
        data/Gemfile
    CHANGED
    
    
    
        data/Guardfile
    CHANGED
    
    | 
         @@ -1,37 +1,5 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # A sample Guardfile
         
     | 
| 
       2 
     | 
    
         
            -
            # More info at https://github.com/guard/guard#readme
         
     | 
| 
       3 
     | 
    
         
            -
             
     | 
| 
       4 
     | 
    
         
            -
            ## Uncomment and set this to only include directories you want to watch
         
     | 
| 
       5 
     | 
    
         
            -
            # directories %w(app lib config test spec features)
         
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
     | 
    
         
            -
            ## Uncomment to clear the screen before every task
         
     | 
| 
       8 
1 
     | 
    
         
             
            clearing :on
         
     | 
| 
       9 
2 
     | 
    
         | 
| 
       10 
     | 
    
         
            -
            ## Guard internally checks for changes in the Guardfile and exits.
         
     | 
| 
       11 
     | 
    
         
            -
            ## If you want Guard to automatically start up again, run guard in a
         
     | 
| 
       12 
     | 
    
         
            -
            ## shell loop, e.g.:
         
     | 
| 
       13 
     | 
    
         
            -
            ##
         
     | 
| 
       14 
     | 
    
         
            -
            ##  $ while bundle exec guard; do echo "Restarting Guard..."; done
         
     | 
| 
       15 
     | 
    
         
            -
            ##
         
     | 
| 
       16 
     | 
    
         
            -
            ## Note: if you are using the `directories` clause above and you are not
         
     | 
| 
       17 
     | 
    
         
            -
            ## watching the project directory ('.'), then you will want to move
         
     | 
| 
       18 
     | 
    
         
            -
            ## the Guardfile to a watched dir and symlink it back, e.g.
         
     | 
| 
       19 
     | 
    
         
            -
            #
         
     | 
| 
       20 
     | 
    
         
            -
            #  $ mkdir config
         
     | 
| 
       21 
     | 
    
         
            -
            #  $ mv Guardfile config/
         
     | 
| 
       22 
     | 
    
         
            -
            #  $ ln -s config/Guardfile .
         
     | 
| 
       23 
     | 
    
         
            -
            #
         
     | 
| 
       24 
     | 
    
         
            -
            # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
         
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
            # Note: The cmd option is now required due to the increasing number of ways
         
     | 
| 
       27 
     | 
    
         
            -
            #       rspec may be run, below are examples of the most common uses.
         
     | 
| 
       28 
     | 
    
         
            -
            #  * bundler: 'bundle exec rspec'
         
     | 
| 
       29 
     | 
    
         
            -
            #  * bundler binstubs: 'bin/rspec'
         
     | 
| 
       30 
     | 
    
         
            -
            #  * spring: 'bin/rspec' (This will use spring if running and you have
         
     | 
| 
       31 
     | 
    
         
            -
            #                          installed the spring binstubs per the docs)
         
     | 
| 
       32 
     | 
    
         
            -
            #  * zeus: 'zeus rspec' (requires the server to be started separately)
         
     | 
| 
       33 
     | 
    
         
            -
            #  * 'just' rspec: 'rspec'
         
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
3 
     | 
    
         
             
            guard :rspec, cmd: "bundle exec rspec" do
         
     | 
| 
       36 
4 
     | 
    
         
             
              require "guard/rspec/dsl"
         
     | 
| 
       37 
5 
     | 
    
         
             
              dsl = Guard::RSpec::Dsl.new(self)
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -1,7 +1,55 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # Xenon
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            [![Gem Version][fury-badge]][fury] [![Build Status][travis-badge]][travis] [![Code Climate][cc-badge]][cc] [![Test Coverage][ccc-badge]][ccc] [![YARD Docs][docs-badge]][docs]
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
       3 
5 
     | 
    
         
             
            An HTTP framework for building RESTful APIs, inspired by [Spray][spray].
         
     | 
| 
       4 
6 
     | 
    
         | 
| 
      
 7 
     | 
    
         
            +
            At the moment I probably wouldn't use this gem for anything you actually depend on because it's _very_ early in its lifecycle. However, this is a flavour of what's in here.
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            ## HTTP Model
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            A set of model objects for the HTTP protocol which can parse and format the strings you typically get into proper objects you can work with. At the moment this covers key things like media types and the most common headers, but it will expand to cover the whole protocol. You can use the model by itself without any other parts of the library.
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            This is how things tend to look:
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 16 
     | 
    
         
            +
            accept = Xenon::Headers::Accept.parse('application/json, application/*; q=0.5')
         
     | 
| 
      
 17 
     | 
    
         
            +
            accept.media_ranges.first.media_type.json? #=> true
         
     | 
| 
      
 18 
     | 
    
         
            +
            accept.media_ranges.last.q #=> 0.5
         
     | 
| 
      
 19 
     | 
    
         
            +
            # etc.
         
     | 
| 
      
 20 
     | 
    
         
            +
            ```
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            ## Routing
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            A tree-based routing approach based on Spray, giving you great flexibility in building APIs and without the need to write extensions, helpers, etc. because everything is a directive and you extend it by simply writing directives! This is highly unstable and in flux at the moment.
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            This is the kind of syntax I'm aiming for which _sort of_ works, but needs a load of changes to allow composition so what's there now is really just a proof of concept of the basic syntax rather than anything close to useful.
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 29 
     | 
    
         
            +
            path_prefix 'users' do
         
     | 
| 
      
 30 
     | 
    
         
            +
              path_end do
         
     | 
| 
      
 31 
     | 
    
         
            +
                get do
         
     | 
| 
      
 32 
     | 
    
         
            +
                  complete 200, User.all
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
                post do
         
     | 
| 
      
 35 
     | 
    
         
            +
                  body as: User do |user|
         
     | 
| 
      
 36 
     | 
    
         
            +
                    user.save!
         
     | 
| 
      
 37 
     | 
    
         
            +
                    respond_with_header 'Location' => "/users/#{user.id}" do
         
     | 
| 
      
 38 
     | 
    
         
            +
                      complete 201, user
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
              path /[0-9]+/ do |user_id|
         
     | 
| 
      
 44 
     | 
    
         
            +
                get do
         
     | 
| 
      
 45 
     | 
    
         
            +
                  complete 200, User.get_by_id(user_id)
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
| 
      
 49 
     | 
    
         
            +
            ```
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            Of course, it'll do all the things you'd expect like support content negotiation properly and return the correct status codes when paths or methods aren't found.
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
       5 
53 
     | 
    
         
             
            ## Installation
         
     | 
| 
       6 
54 
     | 
    
         | 
| 
       7 
55 
     | 
    
         
             
            Add this line to your application's Gemfile:
         
     | 
| 
         @@ -16,17 +64,23 @@ Or install it yourself as: 
     | 
|
| 
       16 
64 
     | 
    
         | 
| 
       17 
65 
     | 
    
         
             
                $ gem install xenon
         
     | 
| 
       18 
66 
     | 
    
         | 
| 
       19 
     | 
    
         
            -
            ## Usage
         
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
            At the moment I probably wouldn't use this gem for anything you actually depend on because it's _very_ early in its lifecycle. However, feel free to have a play around, raise bugs, and contribute if you're interested.
         
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
67 
     | 
    
         
             
            ## Contributing
         
     | 
| 
       24 
68 
     | 
    
         | 
| 
       25 
     | 
    
         
            -
            1. Fork it ( https://github.com/ 
     | 
| 
      
 69 
     | 
    
         
            +
            1. Fork it ( https://github.com/gregbeech/xenon/fork )
         
     | 
| 
       26 
70 
     | 
    
         
             
            2. Create your feature branch (`git checkout -b my-new-feature`)
         
     | 
| 
       27 
71 
     | 
    
         
             
            3. Commit your changes (`git commit -am 'Add some feature'`)
         
     | 
| 
       28 
72 
     | 
    
         
             
            4. Push to the branch (`git push origin my-new-feature`)
         
     | 
| 
       29 
73 
     | 
    
         
             
            5. Create a new Pull Request
         
     | 
| 
       30 
74 
     | 
    
         | 
| 
       31 
75 
     | 
    
         | 
| 
      
 76 
     | 
    
         
            +
            [fury]: http://badge.fury.io/rb/xenon "Xenon at Rubygems"
         
     | 
| 
      
 77 
     | 
    
         
            +
            [fury-badge]: https://badge.fury.io/rb/xenon.svg "Gem Version"
         
     | 
| 
      
 78 
     | 
    
         
            +
            [travis]: https://travis-ci.org/gregbeech/xenon "Xenon at Travis CI"
         
     | 
| 
      
 79 
     | 
    
         
            +
            [travis-badge]: https://travis-ci.org/gregbeech/xenon.svg "Build Status"
         
     | 
| 
      
 80 
     | 
    
         
            +
            [cc]: https://codeclimate.com/github/gregbeech/xenon "Xenon Quality at Code Climate"
         
     | 
| 
      
 81 
     | 
    
         
            +
            [cc-badge]: https://codeclimate.com/github/gregbeech/xenon/badges/gpa.svg "Code Quality"
         
     | 
| 
      
 82 
     | 
    
         
            +
            [ccc]: https://codeclimate.com/github/gregbeech/xenon/coverage "Xenon Coverage at Code Climate"
         
     | 
| 
      
 83 
     | 
    
         
            +
            [ccc-badge]: https://codeclimate.com/github/gregbeech/xenon/badges/coverage.svg "Code Coverage"
         
     | 
| 
      
 84 
     | 
    
         
            +
            [docs]: http://www.rubydoc.info/github/gregbeech/xenon "YARD Docs"
         
     | 
| 
      
 85 
     | 
    
         
            +
            [docs-badge]: http://img.shields.io/badge/yard-docs-blue.svg "YARD Docs"
         
     | 
| 
       32 
86 
     | 
    
         
             
            [spray]: http://spray.io/ "spray"
         
     | 
| 
         @@ -0,0 +1,17 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'xenon'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class HelloWorld < Xenon::API
         
     | 
| 
      
 4 
     | 
    
         
            +
              authenticator = Xenon::BasicAuth.new realm: 'hello world' do |credentials|
         
     | 
| 
      
 5 
     | 
    
         
            +
                credentials.username # should actually auth here!
         
     | 
| 
      
 6 
     | 
    
         
            +
              end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              path '/' do
         
     | 
| 
      
 9 
     | 
    
         
            +
                get do
         
     | 
| 
      
 10 
     | 
    
         
            +
                  authenticate(authenticator) do |user|
         
     | 
| 
      
 11 
     | 
    
         
            +
                    params :greeting do |greeting|
         
     | 
| 
      
 12 
     | 
    
         
            +
                      complete :ok, { greeting => user }
         
     | 
| 
      
 13 
     | 
    
         
            +
                    end
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/xenon.rb
    CHANGED
    
    | 
         @@ -2,16 +2,12 @@ require 'json' 
     | 
|
| 
       2 
2 
     | 
    
         
             
            require 'rack'
         
     | 
| 
       3 
3 
     | 
    
         
             
            require 'active_support/core_ext/string'
         
     | 
| 
       4 
4 
     | 
    
         
             
            require 'xenon/headers'
         
     | 
| 
       5 
     | 
    
         
            -
            require 'xenon/routing'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'xenon/routing/directives'
         
     | 
| 
       6 
6 
     | 
    
         
             
            require 'xenon/version'
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            module Xenon
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
              class Rejection
         
     | 
| 
       11 
     | 
    
         
            -
                ACCEPT = 'ACCEPT'
         
     | 
| 
       12 
     | 
    
         
            -
                HEADER = 'HEADER'
         
     | 
| 
       13 
     | 
    
         
            -
                METHOD = 'METHOD'
         
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
11 
     | 
    
         
             
                attr_reader :reason, :info
         
     | 
| 
       16 
12 
     | 
    
         | 
| 
       17 
13 
     | 
    
         
             
                def initialize(reason, info = {})
         
     | 
| 
         @@ -29,8 +25,7 @@ module Xenon 
     | 
|
| 
       29 
25 
     | 
    
         | 
| 
       30 
26 
     | 
    
         
             
                def initialize(rack_req)
         
     | 
| 
       31 
27 
     | 
    
         
             
                  @rack_req = rack_req
         
     | 
| 
       32 
     | 
    
         
            -
                  @unmatched_path = rack_req.path
         
     | 
| 
       33 
     | 
    
         
            -
                  freeze
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @unmatched_path = rack_req.path.freeze
         
     | 
| 
       34 
29 
     | 
    
         
             
                end
         
     | 
| 
       35 
30 
     | 
    
         | 
| 
       36 
31 
     | 
    
         
             
                def request_method
         
     | 
| 
         @@ -38,11 +33,12 @@ module Xenon 
     | 
|
| 
       38 
33 
     | 
    
         
             
                end
         
     | 
| 
       39 
34 
     | 
    
         | 
| 
       40 
35 
     | 
    
         
             
                def form_hash
         
     | 
| 
       41 
     | 
    
         
            -
                  @rack_req.POST
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @form_hash ||= @rack_req.POST.with_indifferent_access.freeze
         
     | 
| 
       42 
37 
     | 
    
         
             
                end
         
     | 
| 
       43 
38 
     | 
    
         | 
| 
       44 
     | 
    
         
            -
                def  
     | 
| 
       45 
     | 
    
         
            -
                  @rack_req.GET
         
     | 
| 
      
 39 
     | 
    
         
            +
                def param_hash
         
     | 
| 
      
 40 
     | 
    
         
            +
                  puts "GET = #{@rack_req.GET.inspect}"
         
     | 
| 
      
 41 
     | 
    
         
            +
                  @param_hash ||= @rack_req.GET.with_indifferent_access.freeze
         
     | 
| 
       46 
42 
     | 
    
         
             
                end
         
     | 
| 
       47 
43 
     | 
    
         | 
| 
       48 
44 
     | 
    
         
             
                def header(name)
         
     | 
| 
         @@ -56,13 +52,8 @@ module Xenon 
     | 
|
| 
       56 
52 
     | 
    
         | 
| 
       57 
53 
     | 
    
         
             
                def copy(changes = {})
         
     | 
| 
       58 
54 
     | 
    
         
             
                  r = dup
         
     | 
| 
       59 
     | 
    
         
            -
                  changes.each { |k, v| r.instance_variable_set("@#{k}", v) }
         
     | 
| 
       60 
     | 
    
         
            -
                  r 
     | 
| 
       61 
     | 
    
         
            -
                end
         
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
     | 
    
         
            -
                def freeze
         
     | 
| 
       64 
     | 
    
         
            -
                  @unmatched_path.freeze
         
     | 
| 
       65 
     | 
    
         
            -
                  super
         
     | 
| 
      
 55 
     | 
    
         
            +
                  changes.each { |k, v| r.instance_variable_set("@#{k}", v.freeze) }
         
     | 
| 
      
 56 
     | 
    
         
            +
                  r
         
     | 
| 
       66 
57 
     | 
    
         
             
                end
         
     | 
| 
       67 
58 
     | 
    
         
             
              end
         
     | 
| 
       68 
59 
     | 
    
         | 
| 
         @@ -90,14 +81,6 @@ module Xenon 
     | 
|
| 
       90 
81 
     | 
    
         
             
                  @body.freeze
         
     | 
| 
       91 
82 
     | 
    
         
             
                  super
         
     | 
| 
       92 
83 
     | 
    
         
             
                end
         
     | 
| 
       93 
     | 
    
         
            -
             
     | 
| 
       94 
     | 
    
         
            -
                def self.error(status, developer_message = nil)
         
     | 
| 
       95 
     | 
    
         
            -
                  body = { 
         
     | 
| 
       96 
     | 
    
         
            -
                    status: status, 
         
     | 
| 
       97 
     | 
    
         
            -
                    developer_message: developer_message || Rack::Utils::HTTP_STATUS_CODES[status]
         
     | 
| 
       98 
     | 
    
         
            -
                  }
         
     | 
| 
       99 
     | 
    
         
            -
                  Response.new.copy(complete: true, status: status, body: body)
         
     | 
| 
       100 
     | 
    
         
            -
                end
         
     | 
| 
       101 
84 
     | 
    
         
             
              end
         
     | 
| 
       102 
85 
     | 
    
         | 
| 
       103 
86 
     | 
    
         
             
              class Context
         
     | 
| 
         @@ -156,7 +139,7 @@ module Xenon 
     | 
|
| 
       156 
139 
     | 
    
         
             
                end
         
     | 
| 
       157 
140 
     | 
    
         
             
              end
         
     | 
| 
       158 
141 
     | 
    
         | 
| 
       159 
     | 
    
         
            -
              class  
     | 
| 
      
 142 
     | 
    
         
            +
              class API
         
     | 
| 
       160 
143 
     | 
    
         
             
                include Xenon::Routing::Directives
         
     | 
| 
       161 
144 
     | 
    
         | 
| 
       162 
145 
     | 
    
         
             
                DEFAULT_MARSHALLERS = [JsonMarshaller.new]
         
     | 
| 
         @@ -178,8 +161,18 @@ module Xenon 
     | 
|
| 
       178 
161 
     | 
    
         | 
| 
       179 
162 
     | 
    
         
             
                attr_reader :context
         
     | 
| 
       180 
163 
     | 
    
         | 
| 
       181 
     | 
    
         
            -
                 
     | 
| 
       182 
     | 
    
         
            -
                   
     | 
| 
      
 164 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 165 
     | 
    
         
            +
                  def routes
         
     | 
| 
      
 166 
     | 
    
         
            +
                    @routes ||= []
         
     | 
| 
      
 167 
     | 
    
         
            +
                  end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                  def method_missing(name, *args, &block)
         
     | 
| 
      
 170 
     | 
    
         
            +
                    if instance_methods.include?(name)
         
     | 
| 
      
 171 
     | 
    
         
            +
                      routes << [name, args, block]
         
     | 
| 
      
 172 
     | 
    
         
            +
                    else
         
     | 
| 
      
 173 
     | 
    
         
            +
                      super
         
     | 
| 
      
 174 
     | 
    
         
            +
                    end
         
     | 
| 
      
 175 
     | 
    
         
            +
                  end
         
     | 
| 
       183 
176 
     | 
    
         
             
                end
         
     | 
| 
       184 
177 
     | 
    
         | 
| 
       185 
178 
     | 
    
         
             
                def call(env)
         
     | 
| 
         @@ -191,17 +184,23 @@ module Xenon 
     | 
|
| 
       191 
184 
     | 
    
         | 
| 
       192 
185 
     | 
    
         
             
                  accept = @context.request.header('Accept')
         
     | 
| 
       193 
186 
     | 
    
         
             
                  marshaller = accept ? self.class.select_marshaller(accept.media_ranges) : self.class.marshallers.first
         
     | 
| 
       194 
     | 
    
         
            -
             
     | 
| 
       195 
     | 
    
         
            -
             
     | 
| 
       196 
     | 
    
         
            -
             
     | 
| 
       197 
     | 
    
         
            -
             
     | 
| 
       198 
     | 
    
         
            -
             
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                  catch (:complete) do
         
     | 
| 
      
 189 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 190 
     | 
    
         
            +
                      if marshaller.nil?
         
     | 
| 
      
 191 
     | 
    
         
            +
                        @context.rejections << Rejection.new(:accept, { supported: self.class.marshallers.map(&:media_type) })
         
     | 
| 
      
 192 
     | 
    
         
            +
                      else
         
     | 
| 
      
 193 
     | 
    
         
            +
                        self.class.routes.each do |route|
         
     | 
| 
      
 194 
     | 
    
         
            +
                          name, args, block = route
         
     | 
| 
      
 195 
     | 
    
         
            +
                          route_block = proc { instance_eval(&block) }
         
     | 
| 
      
 196 
     | 
    
         
            +
                          send(name, *args, &route_block)
         
     | 
| 
      
 197 
     | 
    
         
            +
                        end
         
     | 
| 
      
 198 
     | 
    
         
            +
                      end
         
     | 
| 
      
 199 
     | 
    
         
            +
                      handle_rejections(@context.rejections)
         
     | 
| 
      
 200 
     | 
    
         
            +
                    rescue => e
         
     | 
| 
      
 201 
     | 
    
         
            +
                      handle_error(e)
         
     | 
| 
       199 
202 
     | 
    
         
             
                    end
         
     | 
| 
       200 
     | 
    
         
            -
                    @context.response = handle_rejections(@context.rejections) unless @context.response.complete?
         
     | 
| 
       201 
     | 
    
         
            -
                  rescue => e
         
     | 
| 
       202 
     | 
    
         
            -
                    @context.response = handle_error(e)
         
     | 
| 
       203 
203 
     | 
    
         
             
                  end
         
     | 
| 
       204 
     | 
    
         
            -
                  @context.response = Response.error(501, 'The response was not completed') unless @context.response.complete?
         
     | 
| 
       205 
204 
     | 
    
         | 
| 
       206 
205 
     | 
    
         
             
                  marshaller ||= self.class.marshallers.first
         
     | 
| 
       207 
206 
     | 
    
         
             
                  resp = @context.response.copy(
         
     | 
| 
         @@ -211,26 +210,40 @@ module Xenon 
     | 
|
| 
       211 
210 
     | 
    
         
             
                end
         
     | 
| 
       212 
211 
     | 
    
         | 
| 
       213 
212 
     | 
    
         
             
                def handle_error(e)
         
     | 
| 
       214 
     | 
    
         
            -
                  puts "handle_error: #{e}"
         
     | 
| 
       215 
     | 
    
         
            -
                   
     | 
| 
      
 213 
     | 
    
         
            +
                  puts "handle_error: #{e.class}: #{e}\n  #{e.backtrace.join("\n  ")}"
         
     | 
| 
      
 214 
     | 
    
         
            +
                  case e
         
     | 
| 
      
 215 
     | 
    
         
            +
                  when ParseError
         
     | 
| 
      
 216 
     | 
    
         
            +
                    fail 400, e.message
         
     | 
| 
      
 217 
     | 
    
         
            +
                  else
         
     | 
| 
      
 218 
     | 
    
         
            +
                    fail 500, e.message # TODO: Only if verbose errors configured
         
     | 
| 
      
 219 
     | 
    
         
            +
                  end
         
     | 
| 
       216 
220 
     | 
    
         
             
                end
         
     | 
| 
       217 
221 
     | 
    
         | 
| 
       218 
222 
     | 
    
         
             
                def handle_rejections(rejections)
         
     | 
| 
       219 
223 
     | 
    
         
             
                  puts "handle_rejections: #{rejections}"
         
     | 
| 
       220 
224 
     | 
    
         
             
                  if rejections.empty?
         
     | 
| 
       221 
     | 
    
         
            -
                     
     | 
| 
      
 225 
     | 
    
         
            +
                    fail 404
         
     | 
| 
       222 
226 
     | 
    
         
             
                  else
         
     | 
| 
       223 
227 
     | 
    
         
             
                    rejection = rejections.first
         
     | 
| 
       224 
228 
     | 
    
         
             
                    case rejection.reason
         
     | 
| 
       225 
     | 
    
         
            -
                    when  
     | 
| 
       226 
     | 
    
         
            -
                       
     | 
| 
       227 
     | 
    
         
            -
                    when  
     | 
| 
       228 
     | 
    
         
            -
                       
     | 
| 
       229 
     | 
    
         
            -
                    when  
     | 
| 
       230 
     | 
    
         
            -
                      supported = rejections.take_while { |r| r.reason ==  
     | 
| 
       231 
     | 
    
         
            -
                       
     | 
| 
       232 
     | 
    
         
            -
                     
     | 
| 
       233 
     | 
    
         
            -
                       
     | 
| 
      
 229 
     | 
    
         
            +
                    when :accept
         
     | 
| 
      
 230 
     | 
    
         
            +
                      fail 406, "Supported media types: #{rejection[:supported].join(", ")}"
         
     | 
| 
      
 231 
     | 
    
         
            +
                    when :header
         
     | 
| 
      
 232 
     | 
    
         
            +
                      fail 400, "Missing required header: #{rejection[:required]}"
         
     | 
| 
      
 233 
     | 
    
         
            +
                    when :method
         
     | 
| 
      
 234 
     | 
    
         
            +
                      supported = rejections.take_while { |r| r.reason == :method }.map { |r| r[:supported].upcase }
         
     | 
| 
      
 235 
     | 
    
         
            +
                      fail 405, "Supported methods: #{supported.join(", ")}"
         
     | 
| 
      
 236 
     | 
    
         
            +
                    when :unauthorized
         
     | 
| 
      
 237 
     | 
    
         
            +
                      if rejection[:scheme]
         
     | 
| 
      
 238 
     | 
    
         
            +
                        challenge = Headers::Challenge.new(rejection[:scheme], rejection.info.except(:scheme))
         
     | 
| 
      
 239 
     | 
    
         
            +
                        respond_with_header Headers::WWWAuthenticate.new(challenge) do
         
     | 
| 
      
 240 
     | 
    
         
            +
                          fail 401
         
     | 
| 
      
 241 
     | 
    
         
            +
                        end
         
     | 
| 
      
 242 
     | 
    
         
            +
                      else
         
     | 
| 
      
 243 
     | 
    
         
            +
                        fail 401
         
     | 
| 
      
 244 
     | 
    
         
            +
                      end
         
     | 
| 
      
 245 
     | 
    
         
            +
                    else
         
     | 
| 
      
 246 
     | 
    
         
            +
                      fail 500
         
     | 
| 
       234 
247 
     | 
    
         
             
                    end
         
     | 
| 
       235 
248 
     | 
    
         
             
                  end
         
     | 
| 
       236 
249 
     | 
    
         
             
                end
         
     | 
    
        data/lib/xenon/auth.rb
    ADDED
    
    | 
         @@ -0,0 +1,63 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'xenon/quoted_string'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Xenon
         
     | 
| 
      
 4 
     | 
    
         
            +
              class BasicCredentials
         
     | 
| 
      
 5 
     | 
    
         
            +
                attr_reader :username, :password
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(username, password)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @username = username
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @password = password
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def token
         
     | 
| 
      
 13 
     | 
    
         
            +
                  Base64.strict_encode64("#{@username}:#{@password}")
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def self.decode(s)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  str = Base64.strict_decode64(s)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  username, password = str.split(':', 2)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  BasicCredentials.new(username, password)
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 23 
     | 
    
         
            +
                  "Basic #{token}"
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              class GenericCredentials
         
     | 
| 
      
 28 
     | 
    
         
            +
                using QuotedString
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                attr_reader :scheme, :token, :params
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def initialize(scheme, token: nil, params: {})
         
     | 
| 
      
 33 
     | 
    
         
            +
                  @scheme = scheme
         
     | 
| 
      
 34 
     | 
    
         
            +
                  @token = token
         
     | 
| 
      
 35 
     | 
    
         
            +
                  @params = params
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 39 
     | 
    
         
            +
                  s = @scheme.dup
         
     | 
| 
      
 40 
     | 
    
         
            +
                  s << ' ' << @token if @token
         
     | 
| 
      
 41 
     | 
    
         
            +
                  s << ' ' << @params.map { |n, v| "#{n}=#{v.quote}" }.join(', ')
         
     | 
| 
      
 42 
     | 
    
         
            +
                  s
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
              class BasicAuth
         
     | 
| 
      
 47 
     | 
    
         
            +
                attr_reader :auth_params
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                def initialize(auth_params = {}, &store)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  @auth_params = auth_params
         
     | 
| 
      
 51 
     | 
    
         
            +
                  @store = store
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                def scheme
         
     | 
| 
      
 55 
     | 
    
         
            +
                  'Basic'
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def call(request)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  header = request.header('Authorization') rescue nil
         
     | 
| 
      
 60 
     | 
    
         
            +
                  @store.call(header.credentials) if header && header.credentials.is_a?(BasicCredentials)
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
              end
         
     | 
| 
      
 63 
     | 
    
         
            +
            end
         
     |