jko_api 0.1.3

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.
@@ -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: []