raisin 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e79d5231cce8b38d975dd0b1bee00793e427d01d
4
+ data.tar.gz: 822e2c2868222cdef4265df60e3852c3c98611c0
5
+ SHA512:
6
+ metadata.gz: d94015c5476fc070466a0299cedd2ce9d58d71a392fcf5e412a1ee12c880a638c84afff29f4782c113dd60347171dd7c0f076dde9c74c05b0bdff0bb03a0f461
7
+ data.tar.gz: 15d704101d562793fcb3ae885438eac47d49c5f44c081ae27fd3cc99949b990c5ab3ddf2c6ee1cd5be5c19116af9ceb02d1949046a49f2d689c3d936067735dd
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .byebug_history
data/Gemfile CHANGED
@@ -2,3 +2,10 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in raisin.gemspec
4
4
  gemspec
5
+
6
+ gem 'byebug'
7
+
8
+ group :test do
9
+ gem 'minitest', '~> 5.10'
10
+ gem 'rake'
11
+ end
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
21
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,163 +1,52 @@
1
1
  # Raisin
2
2
 
3
- Elegant, modular and performant APIs in Rails
3
+ API versioning via the Accept header.
4
4
 
5
5
  ## Installation
6
6
 
7
- Add this line to your application's Gemfile:
7
+ Install as a gem :
8
8
 
9
- gem 'raisin'
10
-
11
- And then execute:
12
-
13
- $ bundle
14
-
15
- Or install it yourself as:
16
-
17
- $ gem install raisin
18
-
19
- ## Usage
20
-
21
- Raisin is composed of two main elements : a router and a lists of API. An API is a file containing your endpoints,
22
- with their paths, implementation and documentation. The router is where you modelize your API, saying which
23
- API goes to which version.
24
-
25
- Under the hood, raisin will generate classes (one for each enpoint), enabling us to use unit test for them, but keeping it transparent for Rails router.
26
-
27
- Here's a basic example of an API
28
-
29
- ```ruby
30
- class UsersAPI < Raisin::API
31
- get '/users' do
32
- expose(:user) { User.all }
33
- end
34
-
35
- post do
36
- expose(:user) { User.new(params[:user]) }
37
-
38
- response do
39
- user.save
40
- respond_with(user)
41
- end
42
- end
43
- end
44
9
  ```
45
-
46
- ### Endpoints
47
-
48
- Each endoint is defined by the http verb used to access it, its path and its implementation. When the path is omitted, raisin will default it to '/'. And since we are in the UsersAPI, raisin will set a prefix to 'users'. So the `post do` is equivalent to `post '/users' do`.
49
- Same for the get method, since its path is the same as the prefix, it can be omitted.
50
-
51
- The `expose` method allows you to create variables elegantly and to access it in your endpoints body or your views (in you gems like jbuilder or rabl-rails). These exposed variables are evaluated in the response block so you have access to everything (like the params object).
52
-
53
- The response block is where your endpoint logic goes. It can be optionnal, as you can see in the get method. If no response is specified, raisin will behave like a a standart rails controller method (thus trying to render a file with tht same name as the endpoint or fallback to API rendering)
54
-
55
- We talk about the name of the endpoint but how is it determine? raisin is smart enough to recognize basic CRUD operations and RESTful endpoint
56
-
57
- ```ruby
58
- class UsersAPI < Raisin::API
59
- get '/users' {} # index
60
- post '/users' {} # create
61
-
62
- put '/users/:id/foo' # foo
63
- end
10
+ gem install raisin
64
11
  ```
65
12
 
66
- You can see theses names if you run `rake routes` in your console. If you prefer to name your endpoint yourself, you can do it by passing an `:as` option
13
+ or add directly to your `Gemfile`
67
14
 
68
- ```ruby
69
- get '/users', as: :my_method_name
70
15
  ```
71
-
72
- ### Namespaces
73
-
74
- You often have endpoint that have a portion of their path in commons, a namespace in raisin. The most used is RESTful applications is the "member" namespace, that look like `/resource_name/:id`.
75
-
76
- raisin provides both generic namespace and member out of the box
77
-
78
- ```ruby
79
- class UsersAPI < Raisin::API
80
- namespace 'foo' do
81
- get '/bar' {} # GET /foo/bar
82
- end
83
-
84
- member do
85
- put do # PUT /users/:id
86
- response do
87
- user.update_attributes(params[:user])
88
- respond_with(user)
89
- end
90
- end
91
- end
92
- end
16
+ gem 'raisin'
93
17
  ```
94
18
 
95
- You see that in the `update` method we used a user variable. This is because you can also expose variable for a whole namespace, which does member automatically (the variable name will be the resource name singularize, 'user' in our example)
96
-
97
- Namespaces can be nested.
98
-
99
- ### Miscellanous
100
-
101
- You can add `single_resource` in your API for single resources.
102
-
103
- Resources can be nested just as regular Rails. For example
104
-
105
- ```ruby
106
- class CommentsAPI < Raisin::API
107
- nested_into_resource :posts
108
-
109
- get '/comments/:id' do # GET /posts/:post_id/comments/:id
110
- expose(:comment) { post.comments.find(params[:id]) }
111
- end
112
- end
113
- ```
19
+ ## Usage
114
20
 
115
- ### Router
21
+ `raisin` allows you to encapsulate your routes within API versions, using a custom `Accept` header to routes them to your controller accordingly.
116
22
 
117
- raisin router is similar to the `routes.rb` in Rails. APIs that appears at the top have precedence on the ones after. Versionning is done by encapsulating APIs inside `version` block. `:all` can be used is a part of your api is accessible for all versions.
23
+ It uses the fact that Rails router is resolved top to bottom.
118
24
 
119
25
  ```ruby
120
- # /app/api/my_api.rb
121
- class MyApi < Raisin::Router
122
- version :v2, using: :header, vendor: 'mycompany' do
123
- mount CommentsApi
124
- end
26
+ # config/routes.rb
125
27
 
126
- version [:v2, :v1] do
127
- mount PostsApi
128
- mount UsersApi
28
+ Rails.application.routes.draw do
29
+ api :v2 do
30
+ resources :users, only: :show
129
31
  end
130
32
 
131
- version :all do
132
- mount LoginApi
33
+ api :v1, default: true do
34
+ resources :users
35
+ get '/users/sign_in', to: 'sessions#new'
133
36
  end
134
37
  end
135
-
136
- # /config/routes.rb
137
-
138
- mount_api MyApi
139
38
  ```
140
39
 
141
- Versionning can be done via the HTTP Accept header (application/vnd.mycompany-v1+json for example) or via the URL
142
- (/v1/users/). When using the header versionning, the vendor must be set. These options can be set globally when configuring raisin.
40
+ Clients using the version `v2` will have access to all the methods from the `v1` version plus their `/users/show` routes will be overriden the the new one define in the first `api` block.
143
41
 
144
42
  ## Configuration
145
43
 
146
44
  ```ruby
147
45
  Raisin.configure do |c|
148
- c.version.using = :header
149
- c.version.vendor = 'mycompany'
46
+ c.vendor = 'mycompany' # replace with your vendor
150
47
  end
151
48
  ```
152
49
 
153
- If you are using versionning via header, you also need to add a middleware to your application stack
154
-
155
- ```ruby
156
- #config/application.rb
157
-
158
- config.middleware.use Raisin::Middleware
159
- ```
160
-
161
50
  ## Contributing
162
51
 
163
52
  1. Fork it
data/Rakefile CHANGED
@@ -1 +1,12 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'lib'
6
+ t.libs << 'test'
7
+ t.pattern = 'test/**/test_*.rb'
8
+ # t.verbose = true
9
+ end
10
+
11
+
12
+ task :default => :test
data/lib/raisin.rb CHANGED
@@ -1,13 +1,19 @@
1
1
  require 'raisin/version'
2
- require 'raisin/configuration'
3
-
4
- require 'raisin/base'
2
+ require 'raisin/mapper'
5
3
  require 'raisin/middleware'
6
4
 
5
+ require 'raisin/railtie' if defined?(Rails)
6
+
7
7
  module Raisin
8
8
  def self.configure
9
- yield Configuration if block_given?
9
+ yield self
10
10
  end
11
- end
12
11
 
13
- require 'raisin/railtie'
12
+ def self.vendor
13
+ @vendor || raise('`vendor` is not configured')
14
+ end
15
+
16
+ def self.vendor=(value)
17
+ @vendor = value
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ require 'raisin/version_constraint'
2
+
3
+ module Raisin
4
+ module Mapper
5
+ def api(version, default: false)
6
+ return unless block_given?
7
+
8
+ version = version.to_s
9
+ raise 'Version is missing in constraint' if version.blank?
10
+ constraint = Raisin::VersionConstraint.new(version) unless default
11
+
12
+ scope(module: version, constraints: constraint) do
13
+ yield
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,29 +4,27 @@ module Raisin
4
4
  # It stores version and format accepted by the client in the env.
5
5
  #
6
6
  class Middleware
7
- ACCEPT_REGEXP = /application\/vnd\.(?<vendor>[a-z]+)-(?<version>v[0-9]+)\+(?<format>[a-z]+)?/
7
+ ACCEPT_REGEXP = /\Aapplication\/vnd\.(?<vendor>[a-z]+).(?<version>v[0-9]+)\+(?<format>[a-z]+)\Z/
8
8
 
9
9
  def initialize(app)
10
- @app = app
11
- @vendor = Configuration.version.vendor
10
+ @app = app
11
+ @vendor = Raisin.vendor
12
12
  end
13
13
 
14
14
  def call(env)
15
- @env = env
16
- verify_accept_header
17
- @app.call(@env)
15
+ extract_version_from_accept_header(ActionDispatch::Request.new(env))
16
+ @app.call(env)
18
17
  end
19
18
 
20
19
  private
21
20
 
22
- def verify_accept_header
23
- if (matches = ACCEPT_REGEXP.match(@env['HTTP_ACCEPT'])) && @vendor == matches[:vendor]
24
- @env['raisin.version'] = matches[:version]
25
- @env['raisin.format'] = "application/#{matches[:format]}"
26
- true
27
- else
28
- false
21
+ def extract_version_from_accept_header(req)
22
+ header = req.get_header('HTTP_ACCEPT'.freeze).to_s.strip
23
+
24
+ if (matches = ACCEPT_REGEXP.match(header)) && @vendor == matches[:vendor]
25
+ req.set_header('raisin.version'.freeze, matches[:version])
26
+ req.format = matches[:format]
29
27
  end
30
28
  end
31
29
  end
32
- end
30
+ end
@@ -1,7 +1,12 @@
1
- require 'raisin/rails/routes'
2
- require 'raisin/rails/request'
3
-
4
1
  module Raisin
5
2
  class Railtie < Rails::Railtie
3
+
4
+ initializer 'raisin.middleware' do |app|
5
+ app.middleware.use Raisin::Middleware
6
+ end
7
+
8
+ initializer 'raisin.patch_rails' do
9
+ ActionDispatch::Routing::Mapper.include(Raisin::Mapper)
10
+ end
6
11
  end
7
- end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module Raisin
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -0,0 +1,11 @@
1
+ module Raisin
2
+ class VersionConstraint
3
+ def initialize(version)
4
+ @version = version
5
+ end
6
+
7
+ def matches?(req)
8
+ @version == req.get_header('raisin.version'.freeze)
9
+ end
10
+ end
11
+ end
data/raisin.gemspec CHANGED
@@ -3,17 +3,23 @@ lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'raisin/version'
5
5
 
6
- Gem::Specification.new do |gem|
7
- gem.name = "raisin"
8
- gem.version = Raisin::VERSION
9
- gem.authors = ["ccocchi"]
10
- gem.email = ["cocchi.c@gmail.com"]
11
- gem.description = %q{An opiniated micro-framework to easily build elegant API on top of Rails}
12
- gem.summary = %q{An opiniated micro-framework to easily build elegant API on top of Rails}
13
- gem.homepage = "https://github.com/ccocchi/raisin"
6
+ Gem::Specification.new do |s|
7
+ s.name = "raisin"
8
+ s.version = Raisin::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["ccocchi"]
11
+ s.email = ["cocchi.c@gmail.com"]
12
+ s.description = %q{API versioning via the Accept header}
13
+ s.summary = %q{Build API with Accept header versioning on top of Rails}
14
+ s.homepage = "https://github.com/ccocchi/raisin"
15
+ s.license = 'MIT'
14
16
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
- gem.require_paths = ["lib"]
17
+ s.required_ruby_version = '>= 2.2.0'
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- test/*`.split("\n")
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_dependency 'actionpack', '~> 5.0'
24
+ s.add_dependency 'activesupport', '~> 5.0'
19
25
  end
data/test/helper.rb ADDED
@@ -0,0 +1,11 @@
1
+ ENV['RAILS_ENV'] = 'test'
2
+ $:.unshift File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'minitest/mock'
5
+ require 'minitest/autorun'
6
+
7
+ require 'active_support/concern'
8
+ require 'action_dispatch'
9
+ require 'raisin'
10
+
11
+ Raisin.vendor = 'acme'
@@ -0,0 +1,29 @@
1
+ require 'helper'
2
+
3
+ class TestMiddleware < Minitest::Test
4
+ def setup
5
+ @middleware = Raisin::Middleware.new(->(env) { :ok })
6
+ end
7
+
8
+ def test_not_vendored_header
9
+ assert_equal :ok, @middleware.call({})
10
+ end
11
+
12
+ def test_wrong_vendor_header
13
+ env = {}
14
+
15
+ assert_equal :ok, @middleware.call(env)
16
+ assert_empty env
17
+ end
18
+
19
+ def test_vendored_header
20
+ env = {
21
+ 'action_dispatch.request.parameters' => {},
22
+ 'HTTP_ACCEPT' => 'application/vnd.acme.v1+json'
23
+ }
24
+
25
+ assert_equal :ok, @middleware.call(env)
26
+ assert_equal 'v1', env['raisin.version']
27
+ refute_empty env['action_dispatch.request.formats']
28
+ end
29
+ end
metadata CHANGED
@@ -1,60 +1,88 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raisin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
5
- prerelease:
4
+ version: 0.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - ccocchi
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-06-12 00:00:00.000000000 Z
13
- dependencies: []
14
- description: An opiniated micro-framework to easily build elegant API on top of Rails
11
+ date: 2017-10-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ description: API versioning via the Accept header
15
42
  email:
16
43
  - cocchi.c@gmail.com
17
44
  executables: []
18
45
  extensions: []
19
46
  extra_rdoc_files: []
20
47
  files:
21
- - .gitignore
48
+ - ".gitignore"
22
49
  - Gemfile
23
- - LICENSE.txt
50
+ - MIT-LICENSE
24
51
  - README.md
25
52
  - Rakefile
26
53
  - lib/raisin.rb
27
- - lib/raisin/base.rb
28
- - lib/raisin/configuration.rb
54
+ - lib/raisin/mapper.rb
29
55
  - lib/raisin/middleware.rb
30
- - lib/raisin/rails/request.rb
31
- - lib/raisin/rails/routes.rb
32
56
  - lib/raisin/railtie.rb
33
- - lib/raisin/routing.rb
34
57
  - lib/raisin/version.rb
58
+ - lib/raisin/version_constraint.rb
35
59
  - raisin.gemspec
60
+ - test/helper.rb
61
+ - test/test_middleware.rb
36
62
  homepage: https://github.com/ccocchi/raisin
37
- licenses: []
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
38
66
  post_install_message:
39
67
  rdoc_options: []
40
68
  require_paths:
41
69
  - lib
42
70
  required_ruby_version: !ruby/object:Gem::Requirement
43
- none: false
44
71
  requirements:
45
- - - ! '>='
72
+ - - ">="
46
73
  - !ruby/object:Gem::Version
47
- version: '0'
74
+ version: 2.2.0
48
75
  required_rubygems_version: !ruby/object:Gem::Requirement
49
- none: false
50
76
  requirements:
51
- - - ! '>='
77
+ - - ">="
52
78
  - !ruby/object:Gem::Version
53
79
  version: '0'
54
80
  requirements: []
55
81
  rubyforge_project:
56
- rubygems_version: 1.8.23
82
+ rubygems_version: 2.6.13
57
83
  signing_key:
58
- specification_version: 3
59
- summary: An opiniated micro-framework to easily build elegant API on top of Rails
60
- test_files: []
84
+ specification_version: 4
85
+ summary: Build API with Accept header versioning on top of Rails
86
+ test_files:
87
+ - test/helper.rb
88
+ - test/test_middleware.rb
data/lib/raisin/base.rb DELETED
@@ -1,69 +0,0 @@
1
- module Raisin
2
-
3
- #
4
- # Abstract class for all actions.
5
- #
6
- class Base < ActionController::Metal
7
- abstract!
8
-
9
- module Compatibility
10
- def cache_store; end
11
- def cache_store=(*); end
12
- def assets_dir=(*); end
13
- def javascripts_dir=(*); end
14
- def stylesheets_dir=(*); end
15
- def page_cache_directory=(*); end
16
- def asset_path=(*); end
17
- def asset_host=(*); end
18
- def relative_url_root=(*); end
19
- def perform_caching=(*); end
20
- def helpers_path=(*); end
21
- def allow_forgery_protection=(*); end
22
- end
23
-
24
- extend Compatibility
25
-
26
- MODULES = [
27
- AbstractController::Helpers,
28
- ActionController::UrlFor,
29
- ActionController::Rendering,
30
- ActionController::Renderers::All,
31
-
32
- ActionController::ConditionalGet,
33
-
34
- ActionController::RackDelegation,
35
- ActionController::MimeResponds,
36
- ActionController::ImplicitRender,
37
- ActionController::DataStreaming,
38
-
39
- AbstractController::Callbacks,
40
- ActionController::Rescue,
41
-
42
- ActionController::Instrumentation,
43
- ]
44
-
45
- if Rails::VERSION::MAJOR >= 4 && Rails::VERSION::MINOR > 0
46
- include AbstractController::Rendering
47
- include ActionView::Rendering
48
- end
49
-
50
- MODULES.each { |mod|
51
- include mod
52
- }
53
-
54
- if Rails::VERSION::MAJOR >= 4
55
- include ActionController::StrongParameters
56
- end
57
-
58
- def _prefixes
59
- @_prefixes ||= begin
60
- parent_prefixes = self.class.parent_prefixes.dup
61
- parent_prefixes.unshift(controller_path)
62
- parent_prefixes.unshift("#{env['raisin.version']}/#{controller_name}") if env.key?('raisin.version')
63
- parent_prefixes
64
- end
65
- end
66
-
67
- ActiveSupport.run_load_hooks(:action_controller, self)
68
- end
69
- end
@@ -1,16 +0,0 @@
1
- module Raisin
2
- class VersionConfig
3
- attr_accessor :vendor
4
- attr_writer :using
5
-
6
- def using
7
- @using || :header
8
- end
9
- end
10
-
11
- module Configuration
12
- def self.version
13
- @version_config ||= VersionConfig.new
14
- end
15
- end
16
- end
@@ -1,20 +0,0 @@
1
- module Raisin
2
- module ApiFormat
3
- def formats
4
- @env["action_dispatch.request.formats"] ||=
5
- if @env.key?('raisin.format')
6
- Array(Mime::Type.lookup(@env['raisin.format']))
7
- elsif parameters[:format]
8
- Array(Mime[parameters[:format]])
9
- elsif use_accept_header && valid_accept_header
10
- accepts
11
- elsif xhr?
12
- [Mime::JS]
13
- else
14
- [Mime::HTML]
15
- end
16
- end
17
- end
18
- end
19
-
20
- ActionDispatch::Request.send(:include, Raisin::ApiFormat)
@@ -1,37 +0,0 @@
1
- require 'raisin/routing'
2
-
3
- module ActionDispatch::Routing
4
- class Mapper
5
-
6
- def api(options, &block)
7
- return unless block_given?
8
-
9
- version = options[:version].to_s
10
- is_default = options.fetch(:default, false)
11
-
12
- raise 'Version is missing in constraint' if version.blank?
13
-
14
- @api_resources = true
15
-
16
- send(:constraints, Raisin::Routing::VersionConstraint.new(version, is_default)) do
17
- send(:scope, module: version) do
18
- yield
19
- end
20
- end
21
- ensure
22
- @api_resources = nil
23
- end
24
-
25
- def resources(*resources, &block)
26
- if @api_resources
27
- options = resources.extract_options!
28
- options[:except] ||= []
29
- options[:except].concat([:new, :edit])
30
- super(*resources, options)
31
- else
32
- super
33
- end
34
- end
35
-
36
- end
37
- end
@@ -1,16 +0,0 @@
1
- module Raisin
2
- module Routing
3
- ALL_VERSIONS = 'all'
4
-
5
- class VersionConstraint
6
- def initialize(version, is_default)
7
- @version = version
8
- @bypass = is_default || version == ALL_VERSIONS
9
- end
10
-
11
- def matches?(req)
12
- @bypass || @version == req.env['raisin.version']
13
- end
14
- end
15
- end
16
- end