jko_api 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3711a73e0bd2f3bb5186bf24f11d4530b9bbeb6c
4
+ data.tar.gz: e3a28ae1fc62c36cf5a3b77ee0f6820873fb9a75
5
+ SHA512:
6
+ metadata.gz: c6adaaad9e11e56f1971934b7522dae6cc7376e18ddae3b3358202ee73dc85452327d5eab3a62c594e05f525ced2320ff1df5b1acbcf03b8e8ce0faedcbadc6a
7
+ data.tar.gz: 26a75d36b762ffa83d8f74f93374b5a843f73fdc9d1c80e3f969afdd68722d2055e9d527e239b722f70477304680f2de002490f19c041bace08d668cd4816bc3
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jko_api.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Jeremy Woertink
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.
@@ -0,0 +1,99 @@
1
+ # JkoApi
2
+
3
+ This is an API versioning gem for rails. Yes, there are a lot of really good and easy ones out there, but the issue with all of them is that when you make a version change, you're copying an entire folder of controllers, specs, and a bunch of routes over just for a single API change.
4
+
5
+ This gem lets you make a single change and reversion an API without copying over a billion files and specs.
6
+
7
+ ## Props
8
+
9
+ 99% of this gem was written by [Justin Ko](https://github.com/justinko). Since he's lazy and won't make a gem from it, I'm doing it for him :stuck_out_tongue: That's why the gem is named after him.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'jko_api'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install jko_api
26
+
27
+ ## Usage
28
+
29
+ In your `config/initializers/jko_api.rb` add
30
+
31
+ ```ruby
32
+ JkoApi.configure do |c|
33
+ # This is the default. You can override this if you need a different controller
34
+ # c.base_controller = Api::ApplicationController
35
+ # This is the folder name where all the api controllers would go
36
+ # c.api_namespace = 'api'
37
+ end
38
+ ```
39
+
40
+ In your `config/routes.rb`
41
+
42
+ ```ruby
43
+ JkoApi.routes self do
44
+ version 1 do
45
+ # put base routes and routes for version 1 here
46
+ resources :foos
47
+ resources :bars
48
+ end
49
+ version 2 # no route changes, maybe just a controller change
50
+ version 3 do
51
+ resources :bars, only: [:index] # only changes :bars in version 3
52
+ end
53
+ end
54
+ ```
55
+
56
+ Place your version 1 controller code in `app/controllers/api/v1`. Controllers should all inherit from `Api::ApplicationController`, or something that inherits from that.
57
+
58
+ ```ruby
59
+ class Api::V1::BarsController < Api::ApplicationController
60
+ def index
61
+ # do stuff
62
+ end
63
+ def show
64
+ # show stuff
65
+ end
66
+ end
67
+ ```
68
+
69
+ Any controllers that you need to alter will go into a new folder in `app/controllers/api/v2`. Notice in this one we inherit from the last version controller.
70
+
71
+ ```ruby
72
+ class Api::V2::BarsController < Api::V1::BarsController
73
+ def index
74
+ # do different stuff
75
+ end
76
+ # no need to redefine show action because it didn't change
77
+ end
78
+ ```
79
+
80
+ We can still do this though
81
+
82
+ ```ruby
83
+ class Api::V3::BarsController < Api::ApplicationController
84
+ def index
85
+ # do different stuff
86
+ end
87
+ end
88
+ ```
89
+
90
+ You can test this all by booting up a simple rails app, then do `curl -H "Accept: application/vnd.api.v1+json" http://localhost:3000/bars`
91
+
92
+
93
+ ## Contributing
94
+
95
+ 1. Fork it ( https://github.com/[my-github-username]/jko_api/fork )
96
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
97
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
98
+ 4. Push to the branch (`git push origin my-new-feature`)
99
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,3 @@
1
+ class Api::ApplicationController < ActionController::Base
2
+ include JkoApi::Controller
3
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jko_api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jko_api"
8
+ spec.version = JkoApi::VERSION
9
+ spec.authors = ["Jeremy Woertink"]
10
+ spec.email = ["jeremywoertink@gmail.com"]
11
+ spec.summary = %q{A Rails API gem}
12
+ spec.description = %q{Some Rails API code written by JustinKo and ported to a badly written gem}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_dependency "responders", "2.1.0"
24
+ spec.add_dependency "rails", ">= 4.2.0", "< 5"
25
+ end
@@ -0,0 +1,69 @@
1
+ require "active_support/all"
2
+ require "active_model/railtie"
3
+ require "action_controller/railtie"
4
+ require "responders"
5
+ require "jko_api/version"
6
+ require "jko_api/class_descendants_builder"
7
+ require "jko_api/constraints"
8
+ require "jko_api/controller"
9
+ require "jko_api/controller_helpers"
10
+ require "jko_api/middleware"
11
+ require "jko_api/request_error"
12
+ require "jko_api/responder"
13
+ require "jko_api/versioning"
14
+ require "jko_api/configuration"
15
+ require "jko_api/util"
16
+ require "jko_api/engine"
17
+
18
+
19
+ module JkoApi
20
+ ACCEPT_HEADER_REGEX = /\Aapplication\/vnd\.api(\.v([0-9]))?\+json\z/
21
+
22
+ mattr_accessor :configuration, instance_accessor: false
23
+ mattr_reader :current_version_number, instance_reader: false
24
+
25
+ def self.configure
26
+ self.configuration ||= Configuration.new
27
+ yield(configuration)
28
+ setup(configuration.base_controller)
29
+ end
30
+
31
+ def self.setup(base_controller)
32
+ Util.stupid_hack!
33
+ ClassDescendantsBuilder.build base_controller, level: max_version_number
34
+ end
35
+
36
+ def self.activated?
37
+ current_version_number.present? && current_version_number > 0
38
+ end
39
+
40
+ def self.reset
41
+ self.current_version_number = nil
42
+ end
43
+
44
+ def self.current_version_number=(version_number)
45
+ @@current_version_number = version_number.to_i
46
+ end
47
+
48
+ def self.min_version_number
49
+ versioning.min_version_number
50
+ end
51
+
52
+ def self.max_version_number
53
+ versioning.max_version_number
54
+ end
55
+
56
+ def self.versioning
57
+ @@versioning || raise('call `.versions` first')
58
+ end
59
+
60
+ def self.versions(context, &block)
61
+ @@versioning = Versioning.new(context, &block)
62
+ end
63
+
64
+ def self.routes(context, &block)
65
+ context.scope(module: JkoApi.configuration.api_namespace, constraints: JkoApi::Constraints, defaults: {format: :json}) do
66
+ JkoApi.versions(context, &block)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,46 @@
1
+ module JkoApi
2
+ class ClassDescendantsBuilder
3
+ LEVEL_REGEX = /\d+/
4
+
5
+ def self.build(base_class, level:)
6
+ base_class.descendants.each do |descendant|
7
+ new(descendant, level).build
8
+ end
9
+ end
10
+
11
+ def initialize(descendant, level)
12
+ @descendant, @level = descendant, level
13
+ end
14
+
15
+ def build
16
+ initial_level.upto(@level - 1) do |level|
17
+ build_descendant(level) unless descendant_defined?(level)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def descendant_defined?(level)
24
+ !!swap_level(level.next).safe_constantize
25
+ end
26
+
27
+ def build_descendant(level)
28
+ namespace(level.next).constantize.const_set(
29
+ swap_level(level.next).demodulize,
30
+ Class.new(swap_level(level).constantize)
31
+ )
32
+ end
33
+
34
+ def namespace(level)
35
+ swap_level(level).deconstantize.presence || 'Object'
36
+ end
37
+
38
+ def swap_level(level)
39
+ @descendant.name.sub LEVEL_REGEX, level.to_s
40
+ end
41
+
42
+ def initial_level
43
+ @descendant.name[LEVEL_REGEX].to_i
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ module JkoApi
2
+ class Configuration
3
+
4
+ attr_accessor :base_controller, :api_namespace
5
+
6
+ def initialize
7
+ @base_controller = Api::ApplicationController
8
+ @api_namespace = 'api'
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ module JkoApi
2
+ module Constraints
3
+ def self.matches?(request)
4
+ request.headers['Accept'] &&
5
+ request.headers['Accept'].match(ACCEPT_HEADER_REGEX) &&
6
+ ::JkoApi.current_version_number.present?
7
+ # TODO: add condition config for specific subdomain
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module JkoApi
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include JkoApi::ControllerHelpers
7
+ skip_authentication # TODO: make this configurable
8
+
9
+ self.responder = JkoApi::Responder
10
+ respond_to :json
11
+ end
12
+
13
+ private
14
+
15
+ def render_json_errors(status, message = status)
16
+ render json: JkoApi::RequestError[status, message], status: status
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module JkoApi
2
+ module ControllerHelpers
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :authenticated
7
+ self.authenticated = false
8
+ append_before_action { raise 'setup authentication' unless authenticated }
9
+ end
10
+
11
+ class_methods do
12
+ def authenticate(*args)
13
+ options, model_classes = args.extract_options!, args
14
+ options.reverse_merge! optional: false
15
+ # This is for use with Devise.
16
+ # TODO: make this configurable
17
+ # options[:fallback_to_devise] = !options.delete(:optional)
18
+ # model_classes.each do |model_class|
19
+ # acts_as_token_authentication_handler_for model_class, options
20
+ # end
21
+ self.authenticated = true
22
+ end
23
+
24
+ def skip_authentication
25
+ self.authenticated = true
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails/railtie'
2
+
3
+ module JkoApi
4
+ class Engine < ::Rails::Engine
5
+ initializer "jko_api.configure_rails_initialization" do |app|
6
+ app.middleware.use JkoApi::Middleware
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ module JkoApi
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ if version_number = extract_version_number(env)
9
+ ::JkoApi.current_version_number = version_number
10
+ else
11
+ ::JkoApi.reset
12
+ end
13
+ @app.call env
14
+ end
15
+
16
+ private
17
+
18
+ def extract_version_number(env)
19
+ accept_header = env['HTTP_ACCEPT']
20
+ return false unless accept_header
21
+ accept_header[ACCEPT_HEADER_REGEX, 2] ||
22
+ accept_header[ACCEPT_HEADER_REGEX] && ::JkoApi.max_version_number
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module JkoApi
2
+ class RequestError
3
+ def self.[](status, message = status)
4
+ new(status, message).representer
5
+ end
6
+
7
+ attr_reader :status, :message
8
+
9
+ def initialize(status, message)
10
+ @status, @message = status, message
11
+ end
12
+
13
+ def representer
14
+ # TODO: integrate this somehow
15
+ end
16
+
17
+ private
18
+
19
+ def errors
20
+ Errors.new(MockRequest.new).tap do |errors|
21
+ errors.add :base, message
22
+ end
23
+ end
24
+
25
+ class MockRequest
26
+ include ::ActiveModel::Model
27
+
28
+ def representable_type
29
+ :requests
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ module JkoApi
2
+ class Responder < ActionController::Responder
3
+ protected
4
+
5
+ def display(model, *args)
6
+ options[:user] ||= controller.current_user
7
+ options[:wrap] ||= controller.wrap
8
+ options[:representer] ||= controller.representer
9
+
10
+ if first_model = Array.wrap(model).first
11
+ options[:wrap] ||= first_model.class.table_name
12
+ end
13
+
14
+ options[:wrap] || raise('set the `wrap` in the controller')
15
+ representer = options[:representer] || first_model.representer
16
+
17
+ if Array === model || ActiveRecord::Relation === model
18
+ representer = representer.for_collection
19
+ end
20
+
21
+ super representer.prepare(model), *args
22
+ end
23
+
24
+ def api_behavior
25
+ raise MissingRenderer.new(format) unless has_renderer?
26
+
27
+ if put?
28
+ display resource, status: :ok, location: api_location
29
+ elsif delete?
30
+ display resource, status: :ok
31
+ else
32
+ super
33
+ end
34
+ end
35
+
36
+ def api_location
37
+ if !options[:location] && controller.controller_name.starts_with?('user_')
38
+ options[:location] = user_resource_api_location
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ def user_resource_api_location
45
+ url_helpers = Rails.application.routes.url_helpers
46
+ url_method = controller.controller_name
47
+ url_method = url_method.singularize unless resources.many?
48
+ url_method = url_method + '_url'
49
+ if resources.many?
50
+ url_helpers.public_send url_method
51
+ else
52
+ url_helpers.public_send url_method, resource
53
+ end
54
+ end
55
+
56
+ def json_resource_errors
57
+ resource.errors
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,18 @@
1
+ module JkoApi
2
+ class Util
3
+
4
+ def self.stupid_hack!
5
+ Rails.application.reload_routes!
6
+ eager_load_api_controllers
7
+ end
8
+
9
+ def self.eager_load_api_controllers
10
+ [Rails.root.join('app', 'controllers')].each do |load_path|
11
+ matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
12
+ Dir.glob("#{load_path}/#{JkoApi.configuration.api_namespace}/**/*.rb").sort.each do |file|
13
+ require_dependency file.sub(matcher, '\1')
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module JkoApi
2
+ VERSION = "0.1.3"
3
+ end
@@ -0,0 +1,27 @@
1
+ module JkoApi
2
+ class Versioning
3
+ def initialize(context, &block)
4
+ @context, @definitions = context, {}
5
+ instance_eval &block
6
+ end
7
+
8
+ def version(number, &block)
9
+ @definitions[number] = block || Proc.new {}
10
+ @context.scope module: "v#{number}", constraints: ->(*) {
11
+ JkoApi.current_version_number == number
12
+ } do
13
+ number.downto(min_version_number) do |i|
14
+ @context.instance_eval &@definitions[i]
15
+ end
16
+ end
17
+ end
18
+
19
+ def min_version_number
20
+ @definitions.keys.min
21
+ end
22
+
23
+ def max_version_number
24
+ @definitions.keys.max
25
+ end
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jko_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Woertink
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: responders
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 2.1.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 2.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 4.2.0
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '5'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 4.2.0
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '5'
75
+ description: Some Rails API code written by JustinKo and ported to a badly written
76
+ gem
77
+ email:
78
+ - jeremywoertink@gmail.com
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - ".gitignore"
84
+ - Gemfile
85
+ - LICENSE.txt
86
+ - README.md
87
+ - Rakefile
88
+ - app/controllers/api/application_controller.rb
89
+ - jko_api.gemspec
90
+ - lib/jko_api.rb
91
+ - lib/jko_api/class_descendants_builder.rb
92
+ - lib/jko_api/configuration.rb
93
+ - lib/jko_api/constraints.rb
94
+ - lib/jko_api/controller.rb
95
+ - lib/jko_api/controller_helpers.rb
96
+ - lib/jko_api/engine.rb
97
+ - lib/jko_api/middleware.rb
98
+ - lib/jko_api/request_error.rb
99
+ - lib/jko_api/responder.rb
100
+ - lib/jko_api/util.rb
101
+ - lib/jko_api/version.rb
102
+ - lib/jko_api/versioning.rb
103
+ homepage: ''
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.4.3
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: A Rails API gem
127
+ test_files: []