instant-api 0.1.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.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +14 -0
  4. data/Gemfile.lock +142 -0
  5. data/README.md +231 -0
  6. data/instant_api.gemspec +20 -0
  7. data/lib/instant_api.rb +13 -0
  8. data/lib/instant_api/controller/build_create.rb +34 -0
  9. data/lib/instant_api/controller/build_destroy.rb +23 -0
  10. data/lib/instant_api/controller/build_edit.rb +11 -0
  11. data/lib/instant_api/controller/build_index.rb +39 -0
  12. data/lib/instant_api/controller/build_new.rb +22 -0
  13. data/lib/instant_api/controller/build_resource.rb +29 -0
  14. data/lib/instant_api/controller/build_show.rb +25 -0
  15. data/lib/instant_api/controller/build_update.rb +46 -0
  16. data/lib/instant_api/controller/builder.rb +54 -0
  17. data/lib/instant_api/controller/exception_handler.rb +38 -0
  18. data/lib/instant_api/controller/parameters.rb +43 -0
  19. data/lib/instant_api/controller/routes.rb +46 -0
  20. data/lib/instant_api/model/active_record_query_builder.rb +36 -0
  21. data/lib/instant_api/model/association_reflector.rb +93 -0
  22. data/lib/instant_api/model/builder.rb +95 -0
  23. data/lib/instant_api/model/collection.rb +43 -0
  24. data/lib/instant_api/model/resource.rb +14 -0
  25. data/lib/instant_api/util/array.rb +15 -0
  26. data/lib/instant_api/version.rb +3 -0
  27. data/spec/dummy/Gemfile +58 -0
  28. data/spec/dummy/Gemfile.lock +193 -0
  29. data/spec/dummy/Rakefile +6 -0
  30. data/spec/dummy/app/assets/images/.keep +0 -0
  31. data/spec/dummy/app/assets/javascripts/application.js +16 -0
  32. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  33. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  34. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  35. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  36. data/spec/dummy/app/mailers/.keep +0 -0
  37. data/spec/dummy/app/models/.keep +0 -0
  38. data/spec/dummy/app/models/a.rb +5 -0
  39. data/spec/dummy/app/models/address.rb +4 -0
  40. data/spec/dummy/app/models/b.rb +4 -0
  41. data/spec/dummy/app/models/c.rb +4 -0
  42. data/spec/dummy/app/models/concerns/.keep +0 -0
  43. data/spec/dummy/app/models/country.rb +4 -0
  44. data/spec/dummy/app/models/d.rb +3 -0
  45. data/spec/dummy/app/models/movie.rb +4 -0
  46. data/spec/dummy/app/models/user.rb +8 -0
  47. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  48. data/spec/dummy/bin/bundle +3 -0
  49. data/spec/dummy/bin/rails +4 -0
  50. data/spec/dummy/bin/rake +4 -0
  51. data/spec/dummy/config.ru +4 -0
  52. data/spec/dummy/config/application.rb +23 -0
  53. data/spec/dummy/config/boot.rb +4 -0
  54. data/spec/dummy/config/database.yml +39 -0
  55. data/spec/dummy/config/environment.rb +5 -0
  56. data/spec/dummy/config/environments/development.rb +29 -0
  57. data/spec/dummy/config/environments/production.rb +80 -0
  58. data/spec/dummy/config/environments/test.rb +36 -0
  59. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  60. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  61. data/spec/dummy/config/initializers/inflections.rb +16 -0
  62. data/spec/dummy/config/initializers/instant_api.rb +3 -0
  63. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  64. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  65. data/spec/dummy/config/initializers/session_store.rb +3 -0
  66. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  67. data/spec/dummy/config/locales/en.yml +23 -0
  68. data/spec/dummy/config/routes.rb +3 -0
  69. data/spec/dummy/db/migrate/20131019140756_create_users.rb +14 -0
  70. data/spec/dummy/db/migrate/20131019141942_create_addresses.rb +14 -0
  71. data/spec/dummy/db/migrate/20131020003152_a.rb +9 -0
  72. data/spec/dummy/db/migrate/20131020003245_b.rb +10 -0
  73. data/spec/dummy/db/migrate/20131020003354_c.rb +11 -0
  74. data/spec/dummy/db/migrate/20131020164202_d.rb +9 -0
  75. data/spec/dummy/db/migrate/20131020164349_ad.rb +8 -0
  76. data/spec/dummy/db/migrate/20140419205834_create_countries.rb +9 -0
  77. data/spec/dummy/db/migrate/20140421005321_create_movies.rb +8 -0
  78. data/spec/dummy/db/migrate/20140421005435_create_countries_movies.rb +8 -0
  79. data/spec/dummy/db/schema.rb +88 -0
  80. data/spec/dummy/db/seeds.rb +7 -0
  81. data/spec/dummy/lib/assets/.keep +0 -0
  82. data/spec/dummy/lib/tasks/.keep +0 -0
  83. data/spec/dummy/log/.keep +0 -0
  84. data/spec/dummy/public/404.html +58 -0
  85. data/spec/dummy/public/422.html +58 -0
  86. data/spec/dummy/public/500.html +57 -0
  87. data/spec/dummy/public/favicon.ico +0 -0
  88. data/spec/dummy/public/robots.txt +5 -0
  89. data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
  90. data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
  91. data/spec/factories/address_factory.rb +8 -0
  92. data/spec/factories/country_factory.rb +5 -0
  93. data/spec/factories/movies_factory.rb +5 -0
  94. data/spec/factories/user_factory.rb +10 -0
  95. data/spec/functional/create_spec.rb +16 -0
  96. data/spec/functional/destroy_spec.rb +21 -0
  97. data/spec/functional/edit_spec.rb +19 -0
  98. data/spec/functional/index_spec.rb +82 -0
  99. data/spec/functional/new_spec.rb +14 -0
  100. data/spec/functional/show_spec.rb +20 -0
  101. data/spec/functional/update_spec.rb +47 -0
  102. data/spec/spec_helper.rb +34 -0
  103. data/spec/support/database_cleaner.rb +20 -0
  104. data/spec/support/helpers.rb +31 -0
  105. data/spec/unit/lib/instant_api/controller/build_create_spec.rb +38 -0
  106. data/spec/unit/lib/instant_api/controller/build_destroy_spec.rb +25 -0
  107. data/spec/unit/lib/instant_api/controller/build_edit_spec.rb +24 -0
  108. data/spec/unit/lib/instant_api/controller/build_index_spec.rb +49 -0
  109. data/spec/unit/lib/instant_api/controller/build_new_spec.rb +18 -0
  110. data/spec/unit/lib/instant_api/controller/build_resource_spec.rb +27 -0
  111. data/spec/unit/lib/instant_api/controller/build_show_spec.rb +24 -0
  112. data/spec/unit/lib/instant_api/controller/build_update_spec.rb +66 -0
  113. data/spec/unit/lib/instant_api/controller/builder_spec.rb +18 -0
  114. data/spec/unit/lib/instant_api/controller/parameters_spec.rb +53 -0
  115. data/spec/unit/lib/instant_api/model/active_record_query_builder_spec.rb +133 -0
  116. data/spec/unit/lib/instant_api/model/builder_spec.rb +237 -0
  117. data/spec/unit/lib/instant_api/model/join_calculator_spec.rb +27 -0
  118. metadata +202 -0
@@ -0,0 +1,23 @@
1
+ module InstantApi::Controller
2
+ class BuildDestroy
3
+
4
+ def initialize(controller, model_class_name = nil)
5
+ @controller = controller
6
+ end
7
+
8
+ def build
9
+ @controller.class_eval(&build_destroy)
10
+ end
11
+
12
+ private
13
+
14
+ def build_destroy
15
+ Proc.new do
16
+ def destroy
17
+ resource.destroy!
18
+ render status: 200, nothing: true
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ require 'instant_api/controller/build_show'
2
+
3
+ module InstantApi::Controller
4
+ class BuildEdit < BuildShow
5
+
6
+ def initialize(controller, model_class_name = nil)
7
+ @controller = controller
8
+ @method = 'edit'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ module InstantApi::Controller
2
+ class BuildIndex
3
+
4
+ attr_reader :model_class_name, :controller
5
+
6
+ def initialize(controller, model_class_name)
7
+ @controller = controller
8
+ @model_class_name = model_class_name
9
+ end
10
+
11
+ def build
12
+ controller.class_eval(&build_index)
13
+ end
14
+
15
+ private
16
+ def build_index
17
+ body = %Q{
18
+ require 'instant_api/model/collection'
19
+ require 'instant_api/controller/parameters'
20
+ def index
21
+ parameters = InstantApi::Controller::Parameters.new(params, request.path)
22
+ collection = InstantApi::Model::Collection.new(#{model_class_name}, parameters)
23
+ response = {
24
+ collection: collection.paginated_collection,
25
+ pagination: {
26
+ count: collection.count,
27
+ page: collection.page,
28
+ per_page: collection.per_page
29
+ }
30
+ }
31
+
32
+ render json: response
33
+ end
34
+ }
35
+
36
+ Proc.new { eval body }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ module InstantApi::Controller
2
+ class BuildNew
3
+
4
+ def initialize(controller, model_class_name = nil)
5
+ @controller = controller
6
+ end
7
+
8
+ def build
9
+ @controller.class_eval(&build_new)
10
+ end
11
+
12
+ private
13
+
14
+ def build_new
15
+ Proc.new do
16
+ def new
17
+ head :ok
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ module InstantApi::Controller
2
+ class BuildResource
3
+ def initialize(controller, model_class_name)
4
+ @controller, @model_class_name = controller, model_class_name
5
+ end
6
+
7
+ def build
8
+ @controller.class_eval(&build_resource)
9
+ end
10
+
11
+ private
12
+
13
+ def build_resource
14
+ # TODO: extract this require
15
+ body = %Q{
16
+ require 'instant_api/model/resource'
17
+ require 'instant_api/controller/parameters'
18
+ def resource
19
+ @resource ||= begin
20
+ parameters = InstantApi::Controller::Parameters.new(params, request.path)
21
+ InstantApi::Model::Resource.new(#{@model_class_name}, parameters).find
22
+ end
23
+ end
24
+ private :resource
25
+ }
26
+ Proc.new { eval body }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module InstantApi::Controller
2
+ class BuildShow
3
+
4
+ def initialize(controller, model_class_name = nil)
5
+ @controller = controller
6
+ @method = 'show'
7
+ end
8
+
9
+ def build
10
+ @controller.class_eval(&build_method)
11
+ end
12
+
13
+ private
14
+
15
+ def build_method
16
+ body = %Q{
17
+ def #{@method}
18
+ render json: resource
19
+ end
20
+ }
21
+
22
+ Proc.new { eval body }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,46 @@
1
+ module InstantApi::Controller
2
+ class BuildUpdate
3
+
4
+ attr_reader :model_class_name, :model_name, :controller
5
+
6
+ def initialize(controller, model_class_name)
7
+ @controller = controller
8
+ @model_class_name = model_class_name
9
+ @model_name = model_class_name.underscore
10
+ end
11
+
12
+ def build
13
+ controller.class_eval(&build_update)
14
+ controller.class_eval(&build_check_strong_parameters)
15
+ end
16
+
17
+ private
18
+
19
+ def build_update
20
+ Proc.new do
21
+ def update
22
+ resource.update_attributes!(check_strong_parameters)
23
+ raise ActiveRecord::RecordInvalid.new(record) if resource.invalid?
24
+ render json: resource
25
+ end
26
+ end
27
+ end
28
+
29
+ def build_check_strong_parameters
30
+ body = if model_class_name.constantize.respond_to?(:strong_parameters)
31
+ "params.require(:#{model_name}).permit(#{model_class_name}.strong_parameters)"
32
+ else
33
+ "params.require(:#{model_name}).permit!"
34
+ end
35
+
36
+ Proc.new do
37
+ eval %Q{
38
+ def check_strong_parameters
39
+ #{body}
40
+ end
41
+ private :check_strong_parameters
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ require 'rails-api'
2
+ require 'instant_api/controller/exception_handler'
3
+ require 'instant_api/controller/build_create'
4
+ require 'instant_api/controller/build_destroy'
5
+ require 'instant_api/controller/build_edit'
6
+ require 'instant_api/controller/build_index'
7
+ require 'instant_api/controller/build_new'
8
+ require 'instant_api/controller/build_resource'
9
+ require 'instant_api/controller/build_update'
10
+ require 'instant_api/controller/build_show'
11
+
12
+ module InstantApi::Controller
13
+ class Builder
14
+
15
+ attr_reader :controller_name, :method_names
16
+ def initialize(controller_name, method_names)
17
+ @controller_name = controller_name
18
+
19
+ @method_names = method_names.map(&:to_sym)
20
+ @method_names << :resource if @method_names != [:index]
21
+ end
22
+
23
+ def build_class
24
+ controller_class_name = "#{controller_name.camelize}Controller"
25
+ controller = Class.new(ActionController::API)
26
+ controller.send(:include, InstantApi::Controller::ExceptionHandler)
27
+ build_methods(controller)
28
+
29
+ Object.const_set(controller_class_name, controller)
30
+ end
31
+
32
+ private
33
+
34
+ CLASSES = {
35
+ create: InstantApi::Controller::BuildCreate,
36
+ destroy: InstantApi::Controller::BuildDestroy,
37
+ edit: InstantApi::Controller::BuildEdit,
38
+ update: InstantApi::Controller::BuildUpdate,
39
+ index: InstantApi::Controller::BuildIndex,
40
+ new: InstantApi::Controller::BuildNew,
41
+ show: InstantApi::Controller::BuildShow,
42
+ edit: InstantApi::Controller::BuildEdit,
43
+ resource: InstantApi::Controller::BuildResource
44
+ }
45
+
46
+ def build_methods(controller)
47
+ model_class_name = controller_name.camelize.singularize
48
+ method_names.each do |name|
49
+ clasz = CLASSES[name]
50
+ clasz.new(controller, model_class_name).build
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ require 'instant_api'
2
+ require 'active_record/validations'
3
+
4
+ module InstantApi::Controller
5
+ module ExceptionHandler
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ rescue_from Exception, with: :render_error
10
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
11
+ rescue_from ActionController::RoutingError, with: :record_not_found
12
+ rescue_from ActionController::UnknownController, with: :record_not_found
13
+ rescue_from ActiveRecord::RecordInvalid, with: :rescue_record_invalid
14
+ end
15
+
16
+ def record_not_found(exception)
17
+ render json: exception_to_json(exception), status: 404
18
+ end
19
+
20
+ def render_error(exception)
21
+ render json: exception, status: 500
22
+ end
23
+
24
+ def rescue_record_invalid(exception)
25
+ render json: exception_to_json(exception), status: 422
26
+ end
27
+
28
+ def exception_to_json(exception)
29
+ hash = case exception
30
+ when ActiveRecord::RecordNotFound
31
+ {field: :id, message: exception.message}
32
+ when ActiveRecord::RecordInvalid
33
+ exception.record.errors.messages.to_a
34
+ end
35
+ { errors: hash }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ module InstantApi::Controller
2
+ class Parameters
3
+ INTERNAL_PARAMS = %w(controller action format _method only_path)
4
+ RAILS_METHODS = %i(new edit)
5
+
6
+ attr_reader :params, :request_path
7
+
8
+ # params is the rails params {"actionF"=>"index", "controller"=>"countries", "id" => 3}
9
+ # request: /users/2/addresses/3/edit
10
+ def initialize(params, request_path)
11
+ @params = params.except(*INTERNAL_PARAMS)
12
+ @request_path = request_path
13
+ end
14
+
15
+ def [](key)
16
+ params[key]
17
+ end
18
+
19
+ def select(&block)
20
+ params.select(&block)
21
+ end
22
+
23
+ # '/users/2/addresses/3/edit' -> [:users, :addresses]
24
+ def resources
25
+ @resources ||= begin
26
+ resources = request_path.strip.
27
+ split('/').
28
+ compact.
29
+ map(&:strip).
30
+ each_with_index.
31
+ select { |_, index| index % 2 == 1 }.
32
+ map { |a, _| a.to_sym }
33
+
34
+ *rest, tail = *resources
35
+ if RAILS_METHODS.index(tail)
36
+ rest
37
+ else
38
+ resources
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ require 'action_dispatch/routing/inspector'
2
+ require 'instant_api/controller/builder'
3
+
4
+ module InstantApi::Controller
5
+ class Routes
6
+
7
+ def initialize
8
+ Rails.application.reload_routes!
9
+ @routes = Rails.application.routes.routes.map do |route|
10
+ ActionDispatch::Routing::RouteWrapper.new(route)
11
+ end.reject(&:internal?)
12
+ end
13
+
14
+ def build_controllers
15
+ controllers_by_routes = Hash.new
16
+ @routes.each do |route|
17
+ controller = route.defaults[:controller]
18
+ list = controllers_by_routes[controller] || []
19
+ controllers_by_routes[controller] = list << route
20
+ end
21
+
22
+ controllers = controllers_by_routes.map do |controller, routes|
23
+ methods = routes.map { |route| route.defaults[:action] }
24
+ [controller, methods]
25
+ end
26
+
27
+ controllers.each do |controller, methods|
28
+ InstantApi::Controller::Builder.new(controller, methods).build_class
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+
36
+ =begin
37
+ routes.each do |route|
38
+ puts [ route.name,
39
+ route.verb,
40
+ route.path,
41
+ route.reqs,
42
+ route.defaults[:action],
43
+ route.defaults[:controller],
44
+ route.json_regexp].join(' - ')
45
+ end;nil
46
+ =end
@@ -0,0 +1,36 @@
1
+ require 'instant_api/model/association_reflector'
2
+
3
+ module InstantApi::Model
4
+ class ActiveRecordQueryBuilder
5
+ attr_reader :model
6
+ def initialize(model)
7
+ @model = model
8
+ end
9
+
10
+ def query(params)
11
+ if has_associations?(params)
12
+ reflector = InstantApi::Model::AssociationReflector.new(params.resources)
13
+ model.joins(reflector.calculate_join).
14
+ where(reflector.calculate_conditions(params))
15
+ else
16
+ model.all
17
+ end
18
+ end
19
+
20
+ def find_first(params)
21
+ result = if has_associations?(params)
22
+ query(params).first
23
+ else
24
+ model.find(params[:id])
25
+ end
26
+ raise ActiveRecord::RecordNotFound.new if !result
27
+ result
28
+ end
29
+
30
+ private
31
+
32
+ def has_associations?(params)
33
+ params.resources.size > 1
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,93 @@
1
+
2
+ #params.resources: [:users, :addresses]
3
+ #request path: /users/1/addresses
4
+ # query: Address.joins(:users)
5
+
6
+ # has_many
7
+ # has_one
8
+ # belongs_to
9
+ # has_and_belongs_to_many
10
+ # Company
11
+ # has_many :employees
12
+ # company.employees PLURAL
13
+ # Employee
14
+ # belongs_to :company
15
+ # employee.company SINGULAR
16
+ # has_one :office
17
+ # company.office
18
+ # has_and_belongs_to_many
19
+ # company.offices
20
+
21
+
22
+ module InstantApi::Model
23
+ class AssociationReflector
24
+
25
+ def initialize(association_list)
26
+ @association_list = association_list
27
+ end
28
+
29
+ def calculate_join
30
+ *associations, resource = *@association_list
31
+ klass = to_class(resource)
32
+ first, *rest = *associations.reverse
33
+
34
+ result = []
35
+ if (assoc = association(klass, first))
36
+ result << assoc.name.to_sym
37
+ klass = to_class(first)
38
+ end
39
+
40
+ if rest
41
+ rest.each do |association_name|
42
+ if (assoc = association(klass, association_name))
43
+ result << {klass.name.downcase.to_sym => assoc.name.to_sym}
44
+ klass = to_class(association_name)
45
+ end
46
+ end
47
+ end
48
+
49
+ result
50
+ end
51
+
52
+ def calculate_conditions(params)
53
+ result = Hash.new
54
+ result[:id] = params[:id] if params[:id]
55
+
56
+ resource, *rest = *params.resources
57
+ klass = to_class(resource)
58
+ rest.map do |association_name|
59
+ if (assoc = association(klass, association_name))
60
+ table = join_table(assoc)
61
+ foreign_key = foreign_key(assoc)
62
+ result[table] = { foreign_key => params[foreign_key] }
63
+ end
64
+
65
+ resource = association_name
66
+ klass = to_class(resource)
67
+ end
68
+
69
+ result
70
+ end
71
+
72
+ def join_table(assoc)
73
+ if assoc.has_and_belongs_to_many?
74
+ assoc.join_table.to_sym
75
+ else
76
+ assoc.name.to_sym
77
+ end
78
+ end
79
+
80
+ def foreign_key(assoc)
81
+ assoc.foreign_key.to_sym
82
+ end
83
+
84
+ def association(klass, name)
85
+ klass.reflect_on_association(name.to_s.singularize.to_sym) ||
86
+ klass.reflect_on_association(name.to_s.pluralize.to_sym)
87
+ end
88
+
89
+ def to_class(symbol)
90
+ symbol.to_s.classify.constantize
91
+ end
92
+ end
93
+ end