xenon 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|