raisin 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []