raisin 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in raisin.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 ccocchi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
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.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Raisin
2
+
3
+ Elegant, modular and performant APIs in Rails
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
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
+ Soon
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/raisin/api.rb ADDED
@@ -0,0 +1,251 @@
1
+ module Raisin
2
+ class MiddlewareStack < ActionDispatch::MiddlewareStack
3
+ class Middleware < ActionDispatch::MiddlewareStack::Middleware
4
+ def update(args)
5
+ @args = args
6
+ end
7
+ end
8
+
9
+ def build(action, app=nil, &block)
10
+ super(app, &block)
11
+ end
12
+ end
13
+
14
+ class API
15
+ class_attribute :middleware_stack
16
+ self.middleware_stack = Raisin::MiddlewareStack.new
17
+
18
+ def self.action(name, klass = ActionDispatch::Request)
19
+ middleware_stack.build(name) do |env|
20
+ self.const_get(name.camelize).new.dispatch(:call, klass.new(env))
21
+ end
22
+ end
23
+
24
+ def self.use(*args, &block)
25
+ middleware_stack.use(*args, &block)
26
+ end
27
+
28
+ def self.action_klass
29
+ @_klass ||= begin
30
+ klass = Class.new(::Raisin::Base)
31
+ klass.send(:include, Raisin::Mixin)
32
+
33
+ if Configuration.enable_auth_by_default && Configuration.default_auth_method
34
+ klass.send(:before_filter, Configuration.default_auth_method)
35
+ end
36
+
37
+ klass.send(:respond_to, *Configuration.response_formats)
38
+ klass
39
+ end
40
+ end
41
+
42
+ def self.action_klass=(klass)
43
+ @_klass = klass
44
+ end
45
+
46
+ def self.reset
47
+ @_routes = []
48
+ @_prefix = self.api_name
49
+ @_namespaces = []
50
+ @_single_resource = false
51
+ end
52
+
53
+ def self.inherited(subclass)
54
+ subclass.reset
55
+ subclass.middleware_stack = self.middleware_stack.dup
56
+ subclass.action_klass = self.action_klass.dup
57
+ super
58
+ end
59
+
60
+ def self.api_name
61
+ @api_name ||= self.name.demodulize.sub(/api/i, '').underscore
62
+ end
63
+
64
+ def self.use_or_update(klass, *args)
65
+ m = middleware_stack.find { |m| m == klass }
66
+ if m
67
+ m.update klass.merge(m.args, args)
68
+ else
69
+ self.use(klass, *args)
70
+ end
71
+ end
72
+
73
+ def self.routes
74
+ @_routes
75
+ end
76
+
77
+ module DSL
78
+ %w(get head post put delete).each do |via|
79
+ class_eval <<-EOF, __FILE__, __LINE__ + 1
80
+ def #{via}(path = '/', options = {}, &block)
81
+ path = normalize_path(path)
82
+ method_name = options.key?(:as) ? options[:as].to_s : extract_method_name(path, :#{via})
83
+
84
+ klass = self.const_set method_name.camelize, Class.new(@_klass, &block)
85
+ klass.send(:expose, current_namespace.exposure, &(current_namespace.lazy_expose)) if current_namespace.try(:expose?)
86
+
87
+ current_namespace.add(method_name) if current_namespace
88
+
89
+ routes << [:#{via}, path, default_route(method_name)]
90
+ end
91
+ EOF
92
+ end
93
+
94
+ def included(&block)
95
+ self.action_klass.class_eval(&block) if block_given?
96
+ end
97
+
98
+ def member(&block)
99
+ namespace(':id') do
100
+ resource = self.api_name.singularize
101
+ expose(resource) { resource.camelize.constantize.send :find, params[:id] }
102
+ instance_eval(&block)
103
+ end
104
+ end
105
+
106
+ def nested_into_resource(parent)
107
+ parent = parent.to_s
108
+ sing = parent.singularize
109
+ id = "#{sing}_id"
110
+
111
+ @_namespaces << Namespace.new("#{parent}/:#{id}")
112
+ current_namespace.expose(sing) { sing.camelize.constantize.send :find, params[id.to_sym]}
113
+ @_namespaces << Namespace.new(@_prefix)
114
+ @_prefix = nil
115
+ end
116
+
117
+ def single_resource
118
+ @_single_resource = true
119
+ @_prefix = @_prefix.singularize if prefix?
120
+ end
121
+
122
+ def prefix(prefix)
123
+ @_prefix = prefix
124
+ end
125
+
126
+ def description(desc)
127
+ # noop
128
+ end
129
+
130
+ def expose(*args, &block)
131
+ current_namespace.expose(*args, &block)
132
+ end
133
+
134
+ def namespace(path, &block)
135
+ path = path.sub(%r(\A/?#{@_prefix}), '') if prefix?
136
+ @_namespaces.push Namespace.new(path)
137
+ yield
138
+ process_filters
139
+ @_namespaces.pop
140
+ end
141
+
142
+ %w(before around after).each do |type|
143
+ class_eval <<-EOF, __FILE__, __LINE__ + 1
144
+ def #{type}(*args, &block)
145
+ return unless current_namespace
146
+ current_namespace.filter(:#{type}, args, &block)
147
+ end
148
+ EOF
149
+ end
150
+
151
+ protected
152
+
153
+ def prefix?
154
+ !!@_prefix
155
+ end
156
+
157
+ def single_resource?
158
+ !!@_single_resource
159
+ end
160
+
161
+ def current_namespace
162
+ @_namespaces.at(0)
163
+ end
164
+
165
+ def current_namespace?
166
+ @_namespaces.length > 0
167
+ end
168
+
169
+ def process_filters
170
+ current_namespace.filters.each_pair { |type, filters|
171
+ filters.each do |name, block|
172
+ superclass.send("#{type}_filter", name, only: current_namespace.methods, &block)
173
+ end
174
+ }
175
+ end
176
+
177
+ def default_route(method)
178
+ "#{modules_prefix}#{self.api_name}##{method}"
179
+ end
180
+
181
+ def modules_prefix
182
+ @modules_prefix ||= begin
183
+ modules = self.name.split('::').slice(0..-2)
184
+ modules.empty? ? '' : "#{modules.map!(&:downcase).join('/')}/"
185
+ end
186
+ end
187
+
188
+ #
189
+ # Get method name from path
190
+ # Example:
191
+ # / => :index
192
+ # /users/:id => :users
193
+ # /users/:id/addresses => :addresses
194
+ #
195
+ def extract_method_name(path, via)
196
+ return method_name_for_single_resource(path, via) if single_resource?
197
+
198
+ if path =~ %r(\A/?#{@_prefix}\z)
199
+ return via == :get ? 'index' : 'create'
200
+ end
201
+
202
+ parts = path.split('/').reverse!
203
+
204
+ return parts.find { |part| !part.start_with?(':') } if parts.first != ':id'
205
+
206
+ case via
207
+ when :get
208
+ 'show'
209
+ when :put
210
+ 'update'
211
+ when :delete
212
+ 'destroy'
213
+ else
214
+ raise "Cannot extract method name from #{path}"
215
+ end
216
+ end
217
+
218
+ def method_name_for_single_resource(path, via)
219
+ if path =~ %r(\A/?#{@_prefix}\z)
220
+ case via
221
+ when :get
222
+ 'show'
223
+ when :post
224
+ 'create'
225
+ when :put
226
+ 'update'
227
+ when :delete
228
+ 'destroy'
229
+ end
230
+ else
231
+ path.split('/').reverse!.last
232
+ end
233
+ end
234
+
235
+ #
236
+ # Creates path with version, namespace and
237
+ # given path, then normalizes it
238
+ #
239
+ def normalize_path(path)
240
+ parts = []
241
+ parts << @_prefix unless !prefix? || path =~ %r(\A/?#{@_prefix})
242
+ parts.concat @_namespaces.reject { |n| path =~ %r(/#{n.path}) }.map!(&:path) if current_namespace?
243
+ parts << path.to_s unless path == '/'
244
+ parts.join('/')
245
+ end
246
+ end
247
+
248
+ extend DSL
249
+
250
+ end
251
+ end
@@ -0,0 +1,237 @@
1
+ module Raisin
2
+ class Base < ActionController::Metal
3
+ abstract!
4
+
5
+ module Compatibility
6
+ def cache_store; end
7
+ def cache_store=(*); end
8
+ def assets_dir=(*); end
9
+ def javascripts_dir=(*); end
10
+ def stylesheets_dir=(*); end
11
+ def page_cache_directory=(*); end
12
+ def asset_path=(*); end
13
+ def asset_host=(*); end
14
+ def relative_url_root=(*); end
15
+ def perform_caching=(*); end
16
+ def helpers_path=(*); end
17
+ def allow_forgery_protection=(*); end
18
+ # def helper_method(*); end
19
+ # def helper(*); end
20
+ end
21
+
22
+ extend Compatibility
23
+
24
+ MODULES = [
25
+ AbstractController::Helpers,
26
+ ActionController::UrlFor,
27
+ ActionController::Rendering,
28
+ ActionController::Renderers::All,
29
+
30
+ ActionController::ConditionalGet,
31
+
32
+ ActionController::RackDelegation,
33
+ ActionController::MimeResponds,
34
+ ActionController::ImplicitRender,
35
+ ActionController::DataStreaming,
36
+
37
+ AbstractController::Callbacks,
38
+ ActionController::Rescue,
39
+
40
+ ActionController::Instrumentation
41
+ ]
42
+
43
+ MODULES.each { |mod|
44
+ include mod
45
+ }
46
+
47
+ def self._expose(name, &block)
48
+
49
+ end
50
+
51
+ def self.controller_path
52
+ @controller_path ||= name && name.sub(/\:\:[^\:]+$/, '').sub(/api$/i, '').underscore
53
+ end
54
+
55
+ #
56
+ # Remove nil prefixes from our anonymous classes
57
+ #
58
+ def _prefixes
59
+ @_prefixes ||= begin
60
+ parent_prefixes = self.class.parent_prefixes
61
+ parent_prefixes.compact.unshift(controller_path)#.map! { |pr| pr.split('/').last }
62
+ end
63
+ end
64
+
65
+ def action_name
66
+ self.class.name.demodulize.underscore
67
+ end
68
+
69
+ #
70
+ # `call` is the only method to be processed. #process is not
71
+ # called in the normal process only in tests
72
+ #
73
+ def process(action, *args)
74
+ super(:call, *args)
75
+ end
76
+
77
+ # class << self
78
+ # attr_internal_reader :routes, :current_namespace
79
+
80
+ # alias :current_namespace? :current_namespace
81
+
82
+ # %w(get head post put delete).each do |via|
83
+ # class_eval <<-EOF, __FILE__, __LINE__ + 1
84
+ # def #{via}(path, options = nil, &block)
85
+ # path = normalize_path(path)
86
+ # method_name = extract_method_name(path, :#{via})
87
+
88
+ # endpoint = Endpoint.new
89
+ # endpoint.instance_eval(&block)
90
+
91
+ # Rails.logger.warn("WARNING: redefinition of method " << method_name) if method_defined?(method_name)
92
+ # define_method(method_name, &(endpoint.response_body))
93
+
94
+ # current_namespace.add(method_name) if current_namespace?
95
+
96
+ # routes << [:#{via}, path, default_route(method_name)]
97
+ # end
98
+ # EOF
99
+ # end
100
+
101
+ # def prefix(prefix)
102
+ # @_prefix = prefix
103
+ # end
104
+
105
+ # def prefix?
106
+ # @_prefix
107
+ # end
108
+
109
+ # def description(desc)
110
+ # # noop
111
+ # end
112
+
113
+ # # def get(path, options = nil, &block)
114
+ # # path = normalize_path(path)
115
+ # # method_name = extract_method_name(path, :get)
116
+
117
+ # # endpoint = Endpoint.new
118
+ # # endpoint.instance_eval(&block)
119
+
120
+ # # define_method(method_name, &(endpoint.response_body))
121
+
122
+ # # current_namespace.add(method_name) if current_namespace?
123
+
124
+ # # routes << [:get, path, default_route(method_name)]
125
+ # # end
126
+ # # alias_method :head, :get
127
+
128
+ # # def post(path, options = nil, &block)
129
+ # # path = normalize_path(path)
130
+ # # method_name = extract_method_name(path)
131
+
132
+ # # define_method(method_name, &block)
133
+
134
+ # # routes << [:post, path, default_route(method_name)]
135
+ # # end
136
+
137
+ # # def put(path, options = nil, &block)
138
+ # # path = normalize_path(path)
139
+ # # method_name = extract_method_name(path)
140
+
141
+ # # define_method(method_name, &block)
142
+
143
+ # # routes << [:put, path, default_route(method_name)]
144
+ # # end
145
+ # # alias_method :patch, :put
146
+
147
+ # # def delete(path, options = nil, &block)
148
+ # # path = normalize_path(path)
149
+ # # method_name = extract_method_name(path)
150
+
151
+ # # define_method(method_name, &block)
152
+
153
+ # # routes << [:delete, path, default_route(method_name)]
154
+ # # end
155
+
156
+ # def namespace(path, &block)
157
+ # path = path.sub(%r(\A/?#{@_prefix}), '') if prefix?
158
+ # old_namespace, @_current_namespace = current_namespace, Namespace.new(path)
159
+ # yield
160
+ # process_filters
161
+ # @_current_namespace = old_namespace
162
+ # end
163
+
164
+ # %w(before around after).each do |type|
165
+ # class_eval <<-EOF, __FILE__, __LINE__ + 1
166
+ # def #{type}(*args, &block)
167
+ # return unless current_namespace?
168
+ # current_namespace.filter(:#{type}, args, &block)
169
+ # end
170
+ # EOF
171
+ # end
172
+
173
+ # protected
174
+
175
+ # def process_filters
176
+ # current_namespace.filters.each_pair { |type, filters|
177
+ # filters.each do |name, block|
178
+ # superclass.send("#{type}_filter", name, only: current_namespace.methods, &block)
179
+ # end
180
+ # }
181
+ # end
182
+
183
+ # def default_route(method)
184
+ # "#{modules_prefix}#{self.api_name}##{method}"
185
+ # end
186
+
187
+ # def modules_prefix
188
+ # @modules_prefix ||= begin
189
+ # modules = self.name.split('::').slice(0..-2)
190
+ # modules.empty? ? '' : "#{modules.join('/')}/"
191
+ # end
192
+ # end
193
+
194
+ # #
195
+ # # Get method name from path
196
+ # # Example:
197
+ # # / => :index
198
+ # # /users/:id => :users
199
+ # # /users/:id/addresses => :addresses
200
+ # #
201
+ # def extract_method_name(path, via)
202
+ # return :index if path =~ %r(\A/?#{@_prefix}\z)
203
+
204
+ # parts = path.split('/').reverse!
205
+
206
+ # return parts.find { |part| !part.start_with?(':') } if parts.first != ':id'
207
+
208
+ # case via
209
+ # when :get
210
+ # :show
211
+ # when :post
212
+ # :create
213
+ # when :put
214
+ # :update
215
+ # when :delete
216
+ # :destroy
217
+ # else
218
+ # raise "Cannot extract method name from #{path}"
219
+ # end
220
+ # end
221
+
222
+ # #
223
+ # # Creates path with version, namespace and
224
+ # # given path, then normalizes it
225
+ # #
226
+ # def normalize_path(path)
227
+ # parts = []
228
+ # parts << @_prefix unless !@_prefix || path =~ %r(\A/?#{@_prefix})
229
+ # parts << current_namespace.path unless !current_namespace? || path =~ %r(/#{current_namespace.path})
230
+ # parts << path.to_s unless path == '/'
231
+ # parts.join('/')
232
+ # end
233
+ # end
234
+
235
+ ActiveSupport.run_load_hooks(:action_controller, self)
236
+ end
237
+ end
@@ -0,0 +1,25 @@
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
+ mattr_accessor :enable_auth_by_default
13
+ @@enable_auth_by_default = false
14
+
15
+ mattr_accessor :default_auth_method
16
+ @@default_auth_method = :authenticate_user! # Devise FTW
17
+
18
+ mattr_accessor :response_formats
19
+ @@response_formats = [:json]
20
+
21
+ def self.version
22
+ @version_config ||= VersionConfig.new
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ module Raisin
2
+ class Endpoint
3
+ include Exposable
4
+
5
+ attr_reader :response_body, :auth_method, :formats
6
+
7
+ def initialize
8
+ @response_body = nil
9
+ @auth_method = nil
10
+ @formats = []
11
+ end
12
+
13
+ def response(&block)
14
+ @response_body = block
15
+ end
16
+
17
+ def has_response?
18
+ !!response_body
19
+ end
20
+
21
+ def desc(description)
22
+ # noop
23
+ end
24
+
25
+ def format(*mime_types)
26
+ @formats.concat mime_types
27
+ end
28
+
29
+ def enable_auth(method = Configuration.default_auth_method)
30
+ return if Configuration.enable_auth_by_default
31
+ @auth_method = method
32
+ end
33
+
34
+ def skip_auth(method = Configuration.default_auth_method)
35
+ return unless Configuration.enable_auth_by_default
36
+ @auth_method = method
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ module Raisin
2
+ module Exposable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_reader :exposure, :lazy_expose
7
+ end
8
+
9
+ def expose(name, &block)
10
+ @exposure = name
11
+ @lazy_expose = block
12
+ end
13
+
14
+ def expose?
15
+ !!@exposure
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module Raisin
2
+ module Middleware
3
+ class Base
4
+ def initialize(app, *args)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ @app.call(env)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ module Raisin
2
+ module Middleware
3
+ class Header < Base
4
+
5
+ def self.merge(base, other)
6
+ base_options, other_options = base.pop, other.pop
7
+ [base.concat(other), base_options.merge!(other_options)]
8
+ end
9
+
10
+ attr_reader :options, :versions
11
+
12
+ def initialize(app, versions, options = {})
13
+ super
14
+ @options = options
15
+ @versions = Array(versions)
16
+ end
17
+
18
+ def call(env)
19
+ @env = env
20
+ return [406, {}, ["You shall not pass!"]] unless verify_http_accept_header
21
+ super
22
+ end
23
+
24
+ private
25
+
26
+ def verify_http_accept_header
27
+ header = @env['HTTP_ACCEPT']
28
+ if (matches = %r{application/vnd\.(?<vendor>[a-z]+)-(?<version>v[0-9]+)\+(?<format>[a-z]+)?}.match(header)) &&
29
+ (versions.include?(matches[:version]) || versions.include?(Raisin::Router::Version::ALL)) &&
30
+ (!options.key?(:vendor) || options[:vendor] == matches[:vendor])
31
+
32
+ @env['raisin.request.formats'] = [Mime::Type.lookup("application/#{matches[:format]}")]
33
+ return true
34
+ end
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ module Raisin
2
+ module Mixin
3
+ extend ActiveSupport::Concern
4
+
5
+ def call
6
+ end
7
+
8
+ module ClassMethods
9
+ def response(&block)
10
+ define_method(:call, &block) if block_given?
11
+ end
12
+
13
+ def desc(description)
14
+ # noop
15
+ end
16
+ alias_method :description, :desc
17
+
18
+ def format(*args)
19
+ self.class_eval <<-EOF, __FILE__, __LINE__ + 1
20
+ respond_to(*#{args})
21
+ EOF
22
+ end
23
+
24
+ def enable_auth(method = nil)
25
+ method ||= Configuration.default_auth_method
26
+ send(:before_filter, method) unless Configuration.enable_auth_by_default
27
+ end
28
+
29
+ def disable_auth(method = nil)
30
+ method ||= Configuration.default_auth_method
31
+ send(:skip_before_filter, method) if Configuration.enable_auth_by_default
32
+ end
33
+
34
+ def expose(name, &block)
35
+ if block_given?
36
+ define_method(name) do |*args|
37
+ ivar = "@#{name}"
38
+
39
+ if instance_variable_defined?(ivar)
40
+ instance_variable_get(ivar)
41
+ else
42
+ instance_variable_set(ivar, instance_exec(block, *args, &block))
43
+ end
44
+ end
45
+ else
46
+ attr_reader name
47
+ end
48
+
49
+ helper_method name
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ module Raisin
2
+ class Namespace
3
+ include Exposable
4
+
5
+ attr_reader :path, :methods, :filters
6
+
7
+ def initialize(path)
8
+ @path = path
9
+ @methods = []
10
+ @filters = {
11
+ before: [],
12
+ after: [],
13
+ around: []
14
+ }
15
+ end
16
+
17
+ def add(method)
18
+ @methods << method
19
+ end
20
+
21
+ def filter(type, *args, &block)
22
+ @filters[type] << [args.first, block]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module Raisin
2
+ module ApiFormat
3
+ def formats
4
+ @env["action_dispatch.request.formats"] ||= @env['raisin.request.formats'] || super
5
+ end
6
+ end
7
+ end
8
+
9
+ ActionDispatch::Request.send(:include, Raisin::ApiFormat)
@@ -0,0 +1,34 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+
4
+ #
5
+ #
6
+ #
7
+ def mount_api(raisin_api)
8
+ raisin_api.routes.each do |method, path, endpoint|
9
+ send(method, path, to: endpoint) # get '/users', to: 'users#index'
10
+ end
11
+ end
12
+ end
13
+
14
+ class RouteSet
15
+ class Dispatcher
16
+
17
+ #
18
+ # Allow to use controller like 'UsersAPI' instead of 'UsersController'
19
+ #
20
+ def controller_reference_with_api(controller_param)
21
+ controller_name = "#{controller_param.camelize}Api"
22
+ unless controller = @controllers[controller_param]
23
+ controller = @controllers[controller_param] =
24
+ ActiveSupport::Dependencies.reference(controller_name)
25
+ end
26
+ controller.get(controller_name)
27
+ rescue NameError
28
+ controller_reference_without_api(controller_param)
29
+ end
30
+
31
+ alias_method_chain :controller_reference, :api
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ require 'raisin/rails/routes'
2
+ require 'raisin/rails/request'
3
+
4
+ module Raisin
5
+ class Railtie < Rails::Railtie
6
+
7
+ # Force routes to be loaded if we are doing any eager load.
8
+ config.before_eager_load { |app| app.reload_routes! }
9
+
10
+ initializer "raisin.initialize" do |app|
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,90 @@
1
+ module Raisin
2
+ class Router
3
+ class Version
4
+ ALL = 'all'
5
+
6
+ attr_reader :version, :type, :options
7
+
8
+ def initialize(version, options = {})
9
+ @version = version.to_s
10
+ @type = options.delete(:using).try(:to_sym) || Configuration.version.using
11
+ @options = { vendor: Configuration.version.vendor }.merge!(options)
12
+
13
+ validate!
14
+ end
15
+
16
+ private
17
+
18
+ def validate!
19
+ raise 'Missing :using options for version' unless type
20
+
21
+ case type
22
+ when :header
23
+ raise 'Missing :vendor options when using header versionning' unless options[:vendor]
24
+ when :path
25
+ raise ':all cannot be used with path versionning' if version == ALL
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.reset
31
+ @_current_version = nil
32
+ @_routes = []
33
+ end
34
+
35
+ #
36
+ # Reset class variables on the subclass when inherited
37
+ #
38
+ def self.inherited(subclass)
39
+ subclass.reset
40
+ end
41
+
42
+ class << self
43
+ attr_internal_accessor :routes, :current_version
44
+
45
+ def mount(api)
46
+ mount_version_middleware(api) if version?(:header)
47
+ self.routes.concat(version?(:path) ? pathed_routes(api.routes) : api.routes)
48
+ end
49
+
50
+ #
51
+ # Set version for current block
52
+ #
53
+ def version(version, options = {}, &block)
54
+ self.current_version = Version.new(version, options)
55
+ yield
56
+ self.current_version = nil
57
+ end
58
+
59
+ private
60
+
61
+ def mount_version_middleware(api)
62
+ api.use_or_update Middleware::Header, self.current_version.version, self.current_version.options
63
+ end
64
+
65
+ def version?(type)
66
+ self.current_version && self.current_version.type == type
67
+ end
68
+
69
+ def pathed_routes(routes)
70
+ self.routes.map! { |via, path, opts|
71
+ path.append('/') unless path.start_with?('/')
72
+ path.append(current_version.version)
73
+ [via, path, opts]
74
+ }
75
+ end
76
+ end
77
+
78
+ # #
79
+ # # Make the API a rack endpoint
80
+ # #
81
+ # def self.call(env)
82
+ # @_route_set.freeze unless @_route_set.frozen?
83
+ # @_route_set.call(env)
84
+ # end
85
+
86
+ # Mount Raisin::Base into the api
87
+ #
88
+
89
+ end
90
+ end
@@ -0,0 +1,12 @@
1
+ module Raisin
2
+ module RSpecApiHelper
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include RSpec::Rails::ControllerExampleGroup
7
+ subject { controller }
8
+ end
9
+
10
+ end
11
+ end
12
+
@@ -0,0 +1,97 @@
1
+ require 'raisin/testing/rspec/unit_helper'
2
+ require 'raisin/testing/rspec/api_helper'
3
+
4
+ module Raisin
5
+ module Testing
6
+ module RSpec
7
+
8
+ #
9
+ #
10
+ #
11
+ module HttpMethods
12
+ %w(get head post put delete).each do |verb|
13
+ class_eval <<-EOF, __FILE__, __LINE__ + 1
14
+ def #{verb}(*args)
15
+ method = self.example.metadata[:current_method].to_s.downcase
16
+ super(method, *args)
17
+ end
18
+ EOF
19
+ end
20
+ end
21
+
22
+ #
23
+ #
24
+ #
25
+ module DSL
26
+ %w(get head post put delete).each do |verb|
27
+ class_eval <<-EOF, __FILE__, __LINE__ + 1
28
+ def #{verb}(path_or_klass, &block)
29
+ self.metadata[:current_klass] = _klass_from_path(path_or_klass, :#{verb})
30
+ self.instance_eval(&block)
31
+ self.metadata.delete(:current_klass)
32
+ end
33
+ EOF
34
+ end
35
+
36
+ def unit(&block)
37
+ _describe_controller(self.metadata[:current_klass], self, &block).tap do |klass|
38
+ klass.send(:include, Raisin::RSpecUnitHelper)
39
+ end
40
+ end
41
+
42
+ def response(format = :json, &block)
43
+ self.metadata[:type] = :api
44
+ _describe_controller(self.metadata[:current_klass], self, &block).tap do |klass|
45
+ klass.send(:include, Raisin::RSpecApiHelper)
46
+ klass.send(:include, HttpMethods)
47
+ klass.send(:render_views) if ::Raisin::Configuration.testing_render_views
48
+ klass.send(:before) {
49
+ request.accept = format.is_a?(Symbol) ? "application/#{format}" : format
50
+ }
51
+ self.metadata.delete(:type)
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ def _klass_from_path(path_or_klass, via)
58
+ api_klass = _find_first_api_klass
59
+
60
+ case path_or_klass
61
+ when String
62
+ method_name = api_klass.send(:extract_method_name, api_klass.send(:normalize_path, path_or_klass), via)
63
+ self.metadata[:current_method] = method_name
64
+ api_klass.const_get(method_name.camelize)
65
+ when Symbol
66
+ self.metadata[:current_method] = path_or_klass
67
+ api_klass.const_get(path_or_klass.to_s.camelize)
68
+ else
69
+ path_or_klass
70
+ end
71
+ end
72
+
73
+ def _find_first_api_klass
74
+ metadata = self.metadata[:example_group]
75
+ klass = nil
76
+
77
+ until metadata.nil? || klass.respond_to?(:new)
78
+ klass = metadata[:description_args].first
79
+ metadata = metadata[:example_group]
80
+ end
81
+
82
+ klass
83
+ end
84
+
85
+ def _describe_controller(klass, parent, &block)
86
+ result = parent.describe(klass, &block)
87
+ result.instance_eval <<-EOF
88
+ def controller_class
89
+ #{klass}
90
+ end
91
+ EOF
92
+ result
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,28 @@
1
+ require 'raisin/testing/rspec/test_request'
2
+
3
+ module Raisin
4
+ module FunctionalTest
5
+ def self.append_features(base)
6
+ base.class_eval do
7
+ include RSpec::Rails::ControllerExampleGroup
8
+ extend ClassMethods
9
+ end
10
+
11
+ super
12
+ end
13
+
14
+ module ClassMethods
15
+ def controller_class
16
+ metadata = self.metadata[:example_group]
17
+ klass = nil
18
+
19
+ until metadata.nil? || klass.respond_to?(:new)
20
+ klass = metadata[:description_args].first
21
+ metadata = metadata[:example_group]
22
+ end
23
+
24
+ klass.respond_to?(:new) ? klass : super
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module ActionController
2
+ class TestRequest < ActionDispatch::TestRequest
3
+
4
+ #
5
+ # Transforms Raisin controller's path as a standart Rails path
6
+ # before assigning parameters to the request
7
+ #
8
+ # Example:
9
+ # controller_path: 'users_api#index', action: 'index'
10
+ # becomes
11
+ # controller_path: 'users', action: 'index'
12
+ #
13
+ def assign_parameters_with_api(routes, controller_path, action, parameters = {})
14
+ controller_path.sub!(/_api/, '')
15
+ controller_path.sub!(/\/?#{action}/, '')
16
+ assign_parameters_without_api(routes, controller_path, action, parameters)
17
+ end
18
+
19
+ alias_method_chain :assign_parameters, :api
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ module Raisin
2
+ module UnitHelper
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def controller_class
7
+ metadata = self.metadata[:example_group]
8
+ klass = nil
9
+
10
+ until metadata.nil? || klass.respond_to?(:new)
11
+ klass = metadata[:description_args].first
12
+ metadata = metadata[:example_group]
13
+ end
14
+
15
+ klass.respond_to?(:new) ? klass : super
16
+ end
17
+ end
18
+
19
+ def controller
20
+ self.class.controller_class.new.tap do |c|
21
+ c.request = request
22
+ c.response = response
23
+ end
24
+ end
25
+
26
+ def request
27
+ @request ||= ActionDispatch::TestRequest.new
28
+ end
29
+
30
+ def response
31
+ @response ||= ActionDispatch::TestResponse.new
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ require 'raisin/testing/rspec/unit_helper'
2
+
3
+ module Raisin
4
+ module UnitTest
5
+ if defined?(RSpec::Rails)
6
+ include RSpec::Rails::RailsExampleGroup
7
+ include RSpec::Rails::Matchers::RedirectTo
8
+ include RSpec::Rails::Matchers::RenderTemplate
9
+ end
10
+
11
+ def self.append_features(base)
12
+ base.class_eval do
13
+ include Raisin::UnitHelper
14
+ subject { controller }
15
+ end
16
+
17
+ super
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # require 'rspec'
2
+ # require 'raisin/testing/rspec/dsl'
3
+ require 'raisin/testing/rspec/unit_test'
4
+ require 'raisin/testing/rspec/functional_test'
5
+
6
+ # require 'raisin/testing/rspec/helper'
7
+
8
+ # RSpec.configure do |c|
9
+ # c.extend(Raisin::Testing::RSpec::DSL)
10
+ # end
@@ -0,0 +1,3 @@
1
+ module Raisin
2
+ VERSION = "0.0.1"
3
+ end
data/lib/raisin.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'raisin/version'
2
+
3
+ require 'raisin/middleware/base'
4
+ require 'raisin/middleware/header'
5
+
6
+ require 'raisin/configuration'
7
+
8
+ require 'raisin/exposable'
9
+ require 'raisin/namespace'
10
+ require 'raisin/mixin'
11
+
12
+ require 'raisin/router'
13
+ require 'raisin/base'
14
+ require 'raisin/api'
15
+
16
+ module Raisin
17
+ def self.configure
18
+ yield Configuration if block_given?
19
+ end
20
+ end
21
+
22
+ require 'raisin/railtie'
data/raisin.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'raisin/version'
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"
14
+
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"]
19
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: raisin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - ccocchi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-13 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: An opiniated micro-framework to easily build elegant API on top of Rails
15
+ email:
16
+ - cocchi.c@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/raisin.rb
27
+ - lib/raisin/api.rb
28
+ - lib/raisin/base.rb
29
+ - lib/raisin/configuration.rb
30
+ - lib/raisin/endpoint.rb
31
+ - lib/raisin/exposable.rb
32
+ - lib/raisin/middleware/base.rb
33
+ - lib/raisin/middleware/header.rb
34
+ - lib/raisin/mixin.rb
35
+ - lib/raisin/namespace.rb
36
+ - lib/raisin/rails/request.rb
37
+ - lib/raisin/rails/routes.rb
38
+ - lib/raisin/railtie.rb
39
+ - lib/raisin/router.rb
40
+ - lib/raisin/testing/rspec.rb
41
+ - lib/raisin/testing/rspec/api_helper.rb
42
+ - lib/raisin/testing/rspec/dsl.rb
43
+ - lib/raisin/testing/rspec/functional_test.rb
44
+ - lib/raisin/testing/rspec/test_request.rb
45
+ - lib/raisin/testing/rspec/unit_helper.rb
46
+ - lib/raisin/testing/rspec/unit_test.rb
47
+ - lib/raisin/version.rb
48
+ - raisin.gemspec
49
+ homepage: https://github.com/ccocchi/raisin
50
+ licenses: []
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.21
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: An opiniated micro-framework to easily build elegant API on top of Rails
73
+ test_files: []