standardapi 1.0.23 → 5.0.0.rc1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 23cfbc54ad1c79fb266ebe6db21945f7688919b4
4
- data.tar.gz: b22e6b36685c565bc4b1bbaa550dd89280375a8e
3
+ metadata.gz: 32b4ca6314bc038faf8b73560d327096f4c4bf53
4
+ data.tar.gz: 1162cb994c7921a7ffc4eea033a40b0e5f4f50b5
5
5
  SHA512:
6
- metadata.gz: aaeb76aaa109f63fb0e49b65f67df7cf0b137804e3a59641fb24b63443acf2635eb9dde7a44513e625bf61cff1239ae1b70c14835fe30840b944e1e9f487eb24
7
- data.tar.gz: c8553dd7b809e8fec5d9f5f03ea580b8bc2058c12d57d23e6a20226b84da764bdd3a9e0ef9df00d8458e8550617ce218b03f90f0a0cb91eb4fa4613d88555f5a
6
+ metadata.gz: deaa898296ded369ca9c9b2289eaab9c803977d9f51f5283bf20d546da33b41e9d6fa2c7750195a9c438a87b59434f29fd0eca79421bce83db6774244276456c
7
+ data.tar.gz: eaeb860e977eee866a9f98ce6ef4b10f697a95a2a6d3e33a286fc390e45102fc287b3139c14ab1a6868b04a115deacabe745c15286f11de76ab86922f6a440c8
@@ -2,20 +2,20 @@ module StandardAPI
2
2
  module Controller
3
3
 
4
4
  def self.included(klass)
5
- klass.hide_action :current_mask
6
5
  klass.helper_method :includes, :orders, :model
7
6
  klass.append_view_path(File.join(File.dirname(__FILE__), 'views'))
8
7
  klass.extend(ClassMethods)
9
8
  end
10
-
9
+
11
10
  def ping
12
- render :text => 'pong'
11
+ render plain: 'pong'
13
12
  end
14
13
 
15
14
  def tables
16
15
  controllers = Dir[Rails.root.join('app/controllers/*_controller.rb')].map{ |path| path.match(/(\w+)_controller.rb/)[1].camelize+"Controller" }.map(&:safe_constantize)
17
16
  controllers.select! { |c| c.ancestors.include?(self.class) && c != self.class }
18
- controllers.map!(&:model).compact!.map!(&:table_name)
17
+ controllers.map!(&:model).compact!
18
+ controllers.map!(&:table_name)
19
19
 
20
20
  render json: controllers
21
21
  end
@@ -39,19 +39,43 @@ module StandardAPI
39
39
  instance_variable_set("@#{model.model_name.singular}", resources.find(params[:id]))
40
40
  end
41
41
 
42
+ def new
43
+ instance_variable_set("@#{model.model_name.singular}", model.new) if model
44
+ end
45
+
42
46
  def create
43
- instance_variable_set("@#{model.model_name.singular}", model.new(model_params))
44
- render :show, status: instance_variable_get("@#{model.model_name.singular}").save ? :created : :bad_request
47
+ record = model.new(model_params)
48
+ instance_variable_set("@#{model.model_name.singular}", record)
49
+
50
+ if record.save
51
+ if request.format == :html
52
+ redirect_to record
53
+ else
54
+ render :show, status: :created
55
+ end
56
+ else
57
+ render :show, status: :bad_request
58
+ end
45
59
  end
46
60
 
47
61
  def update
48
- instance_variable_set("@#{model.model_name.singular}", resources.find(params[:id]))
49
- render :show, status: instance_variable_get("@#{model.model_name.singular}").update_attributes(model_params) ? :ok : :bad_request
62
+ record = resources.find(params[:id])
63
+ instance_variable_set("@#{model.model_name.singular}", record)
64
+
65
+ if record.update_attributes(model_params)
66
+ if request.format == :html
67
+ redirect_to record
68
+ else
69
+ render :show, status: :ok
70
+ end
71
+ else
72
+ render :show, status: :bad_request
73
+ end
50
74
  end
51
75
 
52
76
  def destroy
53
77
  resources.find(params[:id]).destroy!
54
- render nothing: true, status: :no_content
78
+ head :no_content
55
79
  end
56
80
 
57
81
  # Override if you want to support masking
@@ -75,15 +99,27 @@ module StandardAPI
75
99
  end
76
100
 
77
101
  def model_params
78
- params.require(model.model_name.singular).permit(self.send("#{model.model_name.singular}_params"))
102
+ if self.respond_to?("#{model.model_name.singular}_params", true)
103
+ params.require(model.model_name.singular).permit(self.send("#{model.model_name.singular}_params"))
104
+ else
105
+ []
106
+ end
79
107
  end
80
108
 
81
109
  def model_includes
82
- self.send "#{model.model_name.singular}_includes"
110
+ if self.respond_to?("#{model.model_name.singular}_includes", true)
111
+ self.send "#{model.model_name.singular}_includes"
112
+ else
113
+ []
114
+ end
83
115
  end
84
116
 
85
117
  def model_orders
86
- self.send "#{model.model_name.singular}_orders"
118
+ if self.respond_to?("#{model.model_name.singular}_orders", true)
119
+ self.send "#{model.model_name.singular}_orders"
120
+ else
121
+ []
122
+ end
87
123
  end
88
124
 
89
125
  def excludes_for(klass)
@@ -139,4 +175,4 @@ module StandardAPI
139
175
  end
140
176
 
141
177
  end
142
- end
178
+ end
@@ -1,6 +1,6 @@
1
1
  module StandardAPI
2
2
  module Helpers
3
-
3
+
4
4
  def model_partial(record)
5
5
  if lookup_context.exists?(record.model_name.element, record.model_name.plural, true)
6
6
  [record.model_name.plural, record.model_name.element].join('/')
@@ -8,6 +8,98 @@ module StandardAPI
8
8
  'application/record'
9
9
  end
10
10
  end
11
+
12
+ def can_cache?(klass, includes)
13
+ cache_columns = ['cached_at'] + cached_at_columns_for_includes(includes)
14
+ if (cache_columns - klass.column_names).empty?
15
+ true
16
+ else
17
+ false
18
+ end
19
+ end
20
+
21
+ def cache_key(record, includes)
22
+ timestamp_keys = ['cached_at'] + cached_at_columns_for_includes(includes)
23
+ if includes.empty?
24
+ record.cache_key(*timestamp_keys)
25
+ else
26
+ timestamp = record.send(:max_updated_column_timestamp, timestamp_keys)
27
+ "#{record.model_name.cache_key}/#{record.id}-#{digest_hash(sort_hash(includes))}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
28
+ end
29
+ end
30
+
31
+ def can_cache_relation?(klass, relation, subincludes)
32
+ cache_columns = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
33
+ if (cache_columns - klass.column_names).empty?
34
+ true
35
+ else
36
+ false
37
+ end
38
+ end
39
+
40
+ def association_cache_key(record, relation, subincludes)
41
+ timestamp = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
42
+ timestamp.map! { |col| record.send(col) }
43
+ timestamp = timestamp.max
44
+
45
+ case association = record.class.reflect_on_association(relation)
46
+ when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::HasOneReflection, ActiveRecord::Reflection::ThroughReflection
47
+ "#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
48
+ when ActiveRecord::Reflection::BelongsToReflection
49
+ klass = association.options[:polymorphic] ? record.send(association.foreign_type).constantize : association.klass
50
+ if subincludes.empty?
51
+ "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}-#{timestamp.utc.to_s(klass.cache_timestamp_format)}"
52
+ else
53
+ "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}/#{digest_hash(sort_hash(subincludes))}-#{timestamp.utc.to_s(klass.cache_timestamp_format)}"
54
+ end
55
+ else
56
+ raise ArgumentError, 'Unkown association type'
57
+ end
58
+ end
59
+
60
+ def cached_at_columns_for_includes(includes)
61
+ includes.select{|k,v| ![:where, :limit, :order].include?(k.to_sym) }.map { |k, v|
62
+ ["#{k}_cached_at"] + cached_at_columns_for_includes(v).map{|v| "#{k}_#{v}"}
63
+ }.flatten
64
+ end
65
+
66
+ def includes_to_cache_key(relation, subincludes)
67
+ if subincludes.empty?
68
+ relation.to_s
69
+ else
70
+ "#{relation}-#{digest_hash(sort_hash(subincludes))}"
71
+ end
72
+ end
73
+
74
+ def sort_hash(hash)
75
+ hash.keys.sort.reduce({}) do |seed, key|
76
+ if seed[key].is_a?(Hash)
77
+ seed[key] = sort_hash(hash[key])
78
+ else
79
+ seed[key] = hash[key]
80
+ end
81
+ seed
82
+ end
83
+ end
84
+
85
+ def digest_hash(*hashes)
86
+ hashes.compact!
87
+ hashes.map! { |h| sort_hash(h) }
88
+
89
+ digest = Digest::MD5.new()
90
+ hashes.each do |hash|
91
+ hash.each do |key, value|
92
+ digest << key.to_s
93
+ if value.is_a?(Hash)
94
+ digest << digest_hash(value)
95
+ else
96
+ digest << value.to_s
97
+ end
98
+ end
99
+ end
100
+
101
+ digest.hexdigest
102
+ end
11
103
 
12
104
  end
13
105
  end
@@ -15,10 +15,10 @@ module StandardAPI
15
15
  case includes
16
16
  when Array
17
17
  includes.flatten.compact.each { |v| normalized.merge!(normalize(v)) }
18
- when Hash
18
+ when Hash, ActionController::Parameters
19
19
  includes.each_pair do |k, v|
20
20
  if ['where', 'order'].include?(k.to_s) # Where and order are not normalized
21
- normalized[k] = v
21
+ normalized[k] = v.to_h
22
22
  else
23
23
  normalized[k] = normalize(v)
24
24
  end
@@ -55,10 +55,7 @@ module StandardAPI
55
55
  permitted[k] = sanitize(v, permit[k] || {}, true)
56
56
  else
57
57
  if [:raise, nil].include?(Rails.configuration.try(:action_on_unpermitted_includes))
58
- raise(ActionDispatch::ParamsParser::ParseError.new(<<-ERR.squish, nil))
59
- Invalid Include: #{k}"
60
- Set config.action_on_unpermitted_includes = :warm to log instead of raise
61
- ERR
58
+ raise ActionController::UnpermittedParameters.new([k])
62
59
  else
63
60
  Rails.logger.try(:warn, "Invalid Include: #{k}")
64
61
  end
@@ -0,0 +1,34 @@
1
+ require 'msgpack'
2
+
3
+ # QueryEncoding middleware intercepts and parsing the query sting as MessagePack
4
+ # if the `Query-Encoding` header is set to `application/msgpack`
5
+ #
6
+ # Usage:
7
+ #
8
+ # require 'standard_api/middleware/query_encoding'
9
+ #
10
+ # And in the Rails config
11
+ #
12
+ # config.middleware.insert_after Rack::MethodOverride, StandardAPI::Middleware::QueryEncoding
13
+ module StandardAPI
14
+ module Middleware
15
+ class QueryEncoding
16
+ MSGPACK_MIME_TYPE = "application/msgpack".freeze
17
+ HTTP_METHOD_OVERRIDE_HEADER = "HTTP_QUERY_ENCODING".freeze
18
+
19
+ def initialize(app)
20
+ @app = app
21
+ end
22
+
23
+ def call(env)
24
+ if !env[Rack::QUERY_STRING].empty? && env[HTTP_METHOD_OVERRIDE_HEADER] == MSGPACK_MIME_TYPE
25
+ env[Rack::RACK_REQUEST_QUERY_STRING] = env[Rack::QUERY_STRING]
26
+ env[Rack::RACK_REQUEST_QUERY_HASH] = MessagePack.unpack(CGI.unescape(env[Rack::QUERY_STRING]))
27
+ end
28
+
29
+ @app.call(env)
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -9,37 +9,49 @@ module StandardAPI
9
9
  permitted = []
10
10
 
11
11
  case orders
12
- when Hash
12
+ when Hash, ActionController::Parameters
13
13
  orders.each do |key, value|
14
14
  if key.to_s.count('.') == 1
15
15
  key2, key3 = *key.to_s.split('.')
16
- permitted = sanitize({key2.to_sym => { key3.to_sym => value } }, permit)
16
+ permitted << sanitize({key2.to_sym => { key3.to_sym => value } }, permit)
17
17
  elsif permit.include?(key.to_s)
18
- value = value.symbolize_keys if value.is_a?(Hash)
19
- permitted = { key.to_sym => value }
20
- elsif permit.find { |x| x.is_a?(Hash) && x.has_key?(key.to_s) }
21
- subpermit = permit.find { |x| x.is_a?(Hash) && x.has_key?(key.to_s) }[key.to_s]
18
+ value = value.symbolize_keys if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
19
+ permitted << { key.to_sym => value }
20
+ elsif permit.find { |x| (x.is_a?(Hash) || x.is_a?(ActionController::Parameters)) && x.has_key?(key.to_s) }
21
+ subpermit = permit.find { |x| (x.is_a?(Hash) || x.is_a?(ActionController::Parameters)) && x.has_key?(key.to_s) }[key.to_s]
22
22
  sanitized_value = sanitize(value, subpermit)
23
- permitted = { key.to_sym => sanitized_value }
23
+ permitted << { key.to_sym => sanitized_value }
24
24
  else
25
- raise(ActionDispatch::ParamsParser::ParseError.new("Invalid Ordering #{orders.inspect}", nil))
25
+ raise(ActionController::UnpermittedParameters.new([orders]))
26
26
  end
27
27
  end
28
28
  when Array
29
- orders.each { |order| permitted << sanitize(order, permit); }
29
+ orders.each do |order|
30
+ order = sanitize(order, permit)
31
+ if order.is_a?(Array)
32
+ permitted += order
33
+ else
34
+ permitted << order
35
+ end
36
+ end
30
37
  else
31
-
32
38
  if orders.to_s.count('.') == 1
33
39
  key, value = *orders.to_s.split('.')
34
40
  permitted = sanitize({key.to_sym => value.to_sym}, permit)
35
41
  elsif permit.include?(orders.to_s)
36
42
  permitted = orders
37
43
  else
38
- raise(ActionDispatch::ParamsParser::ParseError.new("Invalid Ordering #{orders.inspect}", nil))
44
+ raise(ActionController::UnpermittedParameters.new([orders]))
39
45
  end
40
46
  end
41
47
 
42
- permitted
48
+ if permitted.is_a?(Array) && permitted.length == 1
49
+ permitted.first
50
+ else
51
+ permitted
52
+ end
53
+
54
+ # permitted
43
55
  end
44
56
 
45
57
  end
@@ -4,6 +4,7 @@ module StandardAPI
4
4
  initializer 'standardapi' do
5
5
  ActiveSupport.on_load(:action_view) do
6
6
  ::ActionView::Base.send :include, StandardAPI::Helpers
7
+ ::ActionDispatch::Routing::Mapper.send :include, StandardAPI::RouteHelpers
7
8
  end
8
9
  end
9
10
 
@@ -0,0 +1,26 @@
1
+ module StandardAPI
2
+ module RouteHelpers
3
+
4
+ # Shorthand for adding resources.
5
+ #
6
+ # For example
7
+ #
8
+ # standard_resources :views
9
+ #
10
+ # Is equivilent to:
11
+ #
12
+ # resources :api_keys do
13
+ # get :schema, on: :collection
14
+ # get :calculate, on: :collection
15
+ # end
16
+ def standard_resources(*resources, &block)
17
+ options = resources.extract_options!.dup
18
+
19
+ resources(*resources, options) do
20
+ get :schema, on: :collection
21
+ get :calculate, on: :collection
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -7,7 +7,7 @@ module StandardAPI
7
7
  m = create_model
8
8
  selects = [{ count: :id}, { maximum: :id }, { minimum: :id }, { average: :id }]
9
9
 
10
- get :calculate, select: selects, format: 'json'
10
+ get :calculate, params: {select: selects}, format: :json
11
11
  assert_response :ok
12
12
  assert_equal [[model.count(:id), model.maximum(:id), model.minimum(:id), model.average(:id).to_f]], assigns(:calculations)
13
13
  end
@@ -19,9 +19,14 @@ module StandardAPI
19
19
  selects = [{ count: :id}, { maximum: :id }, { minimum: :id }, { average: :id }]
20
20
  predicate = { id: { gt: m1.id } }
21
21
 
22
- get :calculate, where: predicate, select: selects, format: 'json'
22
+ get :calculate, params: {where: predicate, select: selects}, format: :json
23
23
  assert_response :ok
24
- assert_equal [[model.filter(predicate).count(:id), model.filter(predicate).maximum(:id), model.filter(predicate).minimum(:id), model.filter(predicate).average(:id).to_f]], assigns(:calculations)
24
+ assert_equal [[
25
+ model.filter(predicate).count(:id),
26
+ model.filter(predicate).maximum(:id),
27
+ model.filter(predicate).minimum(:id),
28
+ model.filter(predicate).average(:id).to_f
29
+ ]], assigns(:calculations)
25
30
  end
26
31
 
27
32
  test '#calculate.json mask' do
@@ -3,12 +3,17 @@ module StandardAPI
3
3
  module CreateTests
4
4
  extend ActiveSupport::Testing::Declarative
5
5
 
6
+ def setup
7
+ @request.content_type="application/json"
8
+ super
9
+ end
10
+
6
11
  test '#create.json' do
7
12
  attrs = attributes_for(singular_name, :nested).select{|k,v| !model.readonly_attributes.include?(k.to_s) }
8
13
  create_webmocks(attrs)
9
14
 
10
15
  assert_difference("#{model.name}.count") do
11
- post :create, singular_name => attrs, :format => 'json'
16
+ post :create, params: {singular_name => attrs}, format: :json
12
17
  assert_response :created
13
18
  assert assigns(singular_name)
14
19
 
@@ -18,7 +23,11 @@ module StandardAPI
18
23
  m = assigns(singular_name)
19
24
  view_attributes(m.reload).select { |x| attrs.keys.map(&:to_s).include?(x) }.each do |key, value|
20
25
  message = "Model / Attribute: #{m.class.name}##{key}"
21
- assert_equal normalize_to_json(m, key, attrs[key.to_sym]), json[key.to_s], message
26
+ if value.is_a?(BigDecimal)
27
+ assert_equal normalize_to_json(m, key, attrs[key.to_sym]).to_s.to_f, json[key.to_s].to_s.to_f, message
28
+ else
29
+ assert_equal normalize_to_json(m, key, attrs[key.to_sym]), json[key.to_s], message
30
+ end
22
31
  end
23
32
  end
24
33
  end
@@ -28,13 +37,12 @@ module StandardAPI
28
37
  create_webmocks(attrs)
29
38
 
30
39
  assert_difference("#{model.name}.count") do
31
- post :create, singular_name => attrs, :format => 'json'
40
+ post :create, params: {singular_name => attrs}, format: :json
32
41
  assert_response :created
33
42
  assert assigns(singular_name)
34
43
 
35
44
  json = JSON.parse(response.body)
36
45
  assert json.is_a?(Hash)
37
-
38
46
  m = assigns(singular_name).reload
39
47
  view_attributes(m).select { |x| attrs.keys.map(&:to_s).include?(x) }.each do |key, value|
40
48
  message = "Model / Attribute: #{m.class.name}##{key}"
@@ -44,11 +52,19 @@ module StandardAPI
44
52
  end
45
53
 
46
54
  test '#create.json with invalid attributes' do
55
+ trait = FactoryGirl.factories[singular_name].definition.defined_traits.any? { |x| x.name.to_s == 'invalid' }
56
+
57
+ if !trait
58
+ Rails.logger.try(:warn, "No invalid trait for #{model.name}. Skipping invalid tests")
59
+ warn("No invalid trait for #{model.name}. Skipping invalid tests")
60
+ return
61
+ end
62
+
47
63
  attrs = attributes_for(singular_name, :invalid).select{|k,v| !model.readonly_attributes.include?(k.to_s) }
48
64
  create_webmocks(attrs)
49
65
 
50
66
  assert_difference("#{model.name}.count", 0) do
51
- post :create, singular_name => attrs, :format => 'json'
67
+ post :create, params: {singular_name => attrs}, format: :json
52
68
  assert_response :bad_request
53
69
  json = JSON.parse(response.body)
54
70
  assert json.is_a?(Hash)
@@ -62,7 +78,7 @@ module StandardAPI
62
78
  create_webmocks(attrs)
63
79
 
64
80
  assert_difference("#{model.name}.count") do
65
- post :create, singular_name => attrs, include: includes, :format => 'json'
81
+ post :create, params: {singular_name => attrs, :include => includes}, format: :json
66
82
  assert_response :created
67
83
  assert assigns(singular_name)
68
84
 
@@ -7,7 +7,7 @@ module StandardAPI
7
7
  m = create_model
8
8
 
9
9
  assert_difference("#{model.name}.count", -1) do
10
- delete :destroy, id: m.id, format: 'json'
10
+ delete :destroy, params: { id: m.id }, format: :json
11
11
  assert_response :no_content
12
12
  assert_equal '', response.body
13
13
  end
@@ -21,7 +21,7 @@ module StandardAPI
21
21
  m = create_model
22
22
  @controller.current_mask[plural_name] = { id: m.id + 1 }
23
23
  assert_raises(ActiveRecord::RecordNotFound) do
24
- delete :destroy, id: m.id, format: 'json'
24
+ delete :destroy, params: { id: m.id }, format: :json
25
25
  end
26
26
  @controller.current_mask.delete(plural_name)
27
27
  end
@@ -4,41 +4,41 @@ module StandardAPI
4
4
  extend ActiveSupport::Testing::Declarative
5
5
 
6
6
  test '#index.json' do
7
- get :index, format: 'json'
7
+ get :index, format: :json
8
8
  assert_response :ok
9
- assert_template :index
10
9
  assert_equal model.all.map(&:id).sort, assigns(plural_name).map(&:id).sort
11
10
  assert JSON.parse(response.body).is_a?(Array)
12
11
  end
13
12
 
14
13
  test '#index.json params[:limit]' do
15
- get :index, limit: 1, format: 'json'
14
+ get :index, params: { limit: 1 }, format: :json
16
15
  assert_equal model.limit(1).to_sql, assigns(plural_name).to_sql
17
16
  end
18
17
 
19
18
  test '#index.json params[:where]' do
20
19
  m = create_model
21
- get :index, where: { id: m.id }, format: 'json'
20
+
21
+ get :index, params: { where: { id: m.id } }, format: :json
22
22
  assert_equal [m], assigns(plural_name)
23
23
  end
24
24
 
25
25
  test '#index.json params[:order]' do
26
26
  orders.each do |order|
27
27
  @controller.instance_variable_set('@orders', nil) # Hack for dealing with caching / multiple request per controller life
28
- get :index, order: order, format: 'json'
28
+ get :index, params: { order: order }, format: :json
29
29
  assert_equal model.sort(order).to_sql, assigns(plural_name).to_sql
30
30
  end
31
31
  end
32
32
 
33
33
  test '#index.json params[:offset]' do
34
- get :index, offset: 13, format: 'json'
34
+ get :index, params: { offset: 13 }, format: :json
35
35
  assert_equal model.offset(13).to_sql, assigns(plural_name).to_sql
36
36
  end
37
37
 
38
38
  test '#index.json params[:include]' do
39
39
  travel_to Time.now do
40
40
  create_model
41
- get :index, include: includes, format: 'json'
41
+ get :index, params: { include: includes }, format: :json
42
42
 
43
43
  json = JSON.parse(response.body)[0]
44
44
  assert json.is_a?(Hash)
@@ -67,7 +67,7 @@ module StandardAPI
67
67
 
68
68
  view_attributes(m).each do |key, value|
69
69
  message = "Model / Attribute: #{m.class.name}##{key}"
70
- assert_equal m_json[key.to_s], normalize_to_json(m, key, value)
70
+ assert_equal m_json[key.to_s], normalize_to_json(m, key, value), message
71
71
  end
72
72
 
73
73
  end
@@ -82,7 +82,7 @@ module StandardAPI
82
82
 
83
83
  m = create_model
84
84
  @controller.current_mask[plural_name] = { id: m.id }
85
- get :index, format: 'json'
85
+ get :index, format: :json
86
86
  assert_equal model.where(id: m.id).to_sql, assigns(plural_name).to_sql
87
87
  @controller.current_mask.delete(plural_name)
88
88
  end
@@ -0,0 +1,14 @@
1
+ module StandardAPI
2
+ module TestCase
3
+ module NewTests
4
+ extend ActiveSupport::Testing::Declarative
5
+
6
+ test '#new.json' do
7
+ get :new, format: 'json'
8
+ assert_response :ok
9
+ assert assigns(singular_name)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -6,16 +6,15 @@ module StandardAPI
6
6
  test '#show.json' do
7
7
  m = create_model
8
8
 
9
- get :show, id: m.id, format: 'json'
9
+ get :show, params: {id: m.id}, format: :json
10
10
  assert_response :ok
11
- assert_template :show
12
11
  assert_equal m, assigns(singular_name)
13
12
  assert JSON.parse(response.body).is_a?(Hash)
14
13
  end
15
14
 
16
15
  test '#show.json params[:include]' do
17
16
  m = create_model
18
- get :show, id: m.id, include: includes, format: 'json'
17
+ get :show, params: {id: m.id, include: includes}, format: :json
19
18
 
20
19
  json = JSON.parse(response.body)
21
20
  includes.each do |included|
@@ -56,7 +55,7 @@ module StandardAPI
56
55
  m = create_model
57
56
  @controller.current_mask[plural_name] = { id: m.id + 1 }
58
57
  assert_raises(ActiveRecord::RecordNotFound) do
59
- get :show, id: m.id, format: 'json'
58
+ get :show, params: {id: m.id}, format: :json
60
59
  end
61
60
  @controller.current_mask.delete(plural_name)
62
61
  end
@@ -3,26 +3,46 @@ module StandardAPI
3
3
  module UpdateTests
4
4
  extend ActiveSupport::Testing::Declarative
5
5
 
6
+ def setup
7
+ @request.content_type="application/json"
8
+ super
9
+ end
10
+
6
11
  test '#update.json' do
7
12
  m = create_model
8
13
  attrs = attributes_for(singular_name).select{|k,v| !model.readonly_attributes.include?(k.to_s) }
9
14
  create_webmocks(attrs)
10
15
 
11
- put :update, id: m.id, singular_name => attrs, format: 'json'
12
- assert_response :ok
16
+ put :update, params: {:id => m.id, singular_name => attrs}, format: :json
17
+ assert_response :ok, "Updating #{m.class.name} with #{attrs.inspect}"
13
18
 
14
19
  view_attributes(m.reload).select { |x| attrs.keys.map(&:to_s).include?(x) }.each do |key, value|
15
20
  message = "Model / Attribute: #{m.class.name}##{key}"
16
- assert_equal normalize_attribute(m, key, attrs[key.to_sym]), value, message
21
+ if value.is_a?(BigDecimal)
22
+ assert_equal normalize_attribute(m, key, attrs[key.to_sym]).to_s.to_f, value.to_s.to_f, message
23
+ else
24
+ assert_equal normalize_attribute(m, key, attrs[key.to_sym]), value, message
25
+ end
17
26
  end
18
27
  assert JSON.parse(@response.body).is_a?(Hash)
19
28
  end
20
29
 
30
+ test '#update.html redirects to #show.html' do
31
+ return if @controller.method(:update).owner != StandardAPI
32
+
33
+ m = create_model
34
+ attrs = attributes_for(singular_name).select{|k,v| !model.readonly_attributes.include?(k.to_s) }
35
+ create_webmocks(attrs)
36
+
37
+ put :update, params: {:id => m.id, singular_name => attrs}, format: :html
38
+ assert_redirected_to m
39
+ end
40
+
21
41
  test '#update.json with nested attributes' do
22
42
  m = create_model
23
43
  attrs = attributes_for(singular_name, :nested).select{|k,v| !model.readonly_attributes.include?(k.to_s) }
24
44
  create_webmocks(attrs)
25
- put :update, id: m.id, singular_name => attrs, format: 'json'
45
+ put :update, params: {:id => m.id, singular_name => attrs}, format: :json
26
46
  assert_response :ok
27
47
 
28
48
  # (m.attribute_names & attrs.keys.map(&:to_s)).each do |test_key|
@@ -34,12 +54,20 @@ module StandardAPI
34
54
  end
35
55
 
36
56
  test '#update.json with invalid attributes' do
57
+ trait = FactoryGirl.factories[singular_name].definition.defined_traits.any? { |x| x.name.to_s == 'invalid' }
58
+
59
+ if !trait
60
+ Rails.logger.try(:warn, "No invalid trait for #{model.name}. Skipping invalid tests")
61
+ warn("No invalid trait for #{model.name}. Skipping invalid tests")
62
+ return
63
+ end
64
+
37
65
  m = create_model
38
66
  attrs = attributes_for(singular_name, :invalid).select{|k,v| !model.readonly_attributes.include?(k.to_s) }
39
67
  create_webmocks(attrs)
40
68
 
41
- put :update, id: m.id, singular_name => attrs, format: 'json'
42
- assert_response :bad_request
69
+ put :update, params: {:id => m.id, singular_name => attrs}, format: :json
70
+ assert_response :bad_request, "Updating #{m.class.name} with invalid attributes #{attrs.inspect}"
43
71
  assert JSON.parse(@response.body)['errors']
44
72
  end
45
73
 
@@ -49,7 +77,7 @@ module StandardAPI
49
77
  attrs = attributes_for(singular_name, :nested).select{|k,v| !model.readonly_attributes.include?(k) }
50
78
  create_webmocks(attrs)
51
79
 
52
- put :update, id: m.id, include: includes, singular_name => attrs, format: 'json'
80
+ put :update, params: {:id => m.id, :include => includes, singular_name => attrs}, format: :json
53
81
 
54
82
  json = JSON.parse(response.body)
55
83
  includes.each do |included|
@@ -75,7 +103,7 @@ module StandardAPI
75
103
 
76
104
  view_attributes(m).each do |key, value|
77
105
  message = "Model / Attribute: #{m.class.name}##{key}"
78
- assert_equal m_json[key.to_s], normalize_to_json(m, key, value)
106
+ assert_equal m_json[key.to_s], normalize_to_json(m, key, value), message
79
107
  end
80
108
 
81
109
  end
@@ -91,7 +119,7 @@ module StandardAPI
91
119
  m = create_model
92
120
  @controller.current_mask[plural_name] = { id: m.id + 1 }
93
121
  assert_raises(ActiveRecord::RecordNotFound) do
94
- put :update, id: m.id, format: 'json'
122
+ put :update, params: {id: m.id}, format: 'json'
95
123
  end
96
124
  @controller.current_mask.delete(plural_name)
97
125
  end
@@ -1,6 +1,7 @@
1
1
  require 'active_support/test_case'
2
2
 
3
3
  require File.expand_path(File.join(__FILE__, '../test_case/calculate_tests'))
4
+ require File.expand_path(File.join(__FILE__, '../test_case/new_tests'))
4
5
  require File.expand_path(File.join(__FILE__, '../test_case/create_tests'))
5
6
  require File.expand_path(File.join(__FILE__, '../test_case/destroy_tests'))
6
7
  require File.expand_path(File.join(__FILE__, '../test_case/index_tests'))
@@ -78,12 +79,11 @@ module StandardAPI::TestCase
78
79
 
79
80
  def normalize_to_json(record, attribute, value)
80
81
  value = normalize_attribute(record, attribute, value)
81
-
82
82
  return nil if value.nil?
83
83
 
84
- if model.column_types[attribute].is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Decimal)
84
+ if model.columns_hash[attribute].is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Decimal)
85
85
  "#{value.to_f}"
86
- elsif model.column_types[attribute].is_a?(ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter)
86
+ elsif value.is_a?(DateTime) #model.columns_hash[attribute].is_a?(ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter)
87
87
  value.in_time_zone.as_json
88
88
  else
89
89
  value.as_json
@@ -111,7 +111,7 @@ module StandardAPI::TestCase
111
111
  value = m.send(filter[0])
112
112
 
113
113
  assert_predicate = -> (predicate) {
114
- get :index, where: predicate, format: 'json'
114
+ get :index, params: {where: predicate}, format: 'json'
115
115
  assert_equal model.filter(predicate).to_sql, assigns(plural_name).to_sql
116
116
  }
117
117
 
@@ -5,47 +5,55 @@ record.attributes.each do |name, value|
5
5
  end
6
6
 
7
7
  includes.each do |inc, subinc|
8
- next if ['where', 'order'].include?(inc.to_s)
9
-
10
- association = record.class.reflect_on_association(inc)
11
- if association
12
- collection = [:has_many, :has_and_belongs_to_many].include?(association.macro)
13
-
14
- if collection
8
+ next if [:where, :order, :limit].include?(inc.to_sym)
9
+
10
+ case association = record.class.reflect_on_association(inc)
11
+ when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::ThroughReflection
12
+ can_cache = can_cache_relation?(record.class, inc, subinc)
13
+ json.cache_if!(can_cache, can_cache ? association_cache_key(record, inc, subinc) : nil) do
15
14
  partial = model_partial(association.klass)
16
15
  json.set! inc do
17
- json.array! record.send(inc), partial: partial, as: partial.split('/').last, locals: { includes: subinc }
16
+ json.array! record.send(inc).filter(subinc[:where]).limit(subinc[:limit]).sort(subinc[:order]), partial: partial, as: partial.split('/').last, locals: { includes: subinc }
18
17
  end
19
- else
20
-
21
- if record.send(inc).nil?
18
+ end
19
+
20
+ when ActiveRecord::Reflection::BelongsToReflection, ActiveRecord::Reflection::HasOneReflection
21
+ can_cache = can_cache_relation?(record.class, inc, subinc)
22
+ if association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
23
+ can_cache = can_cache && !record.send(association.foreign_key).nil?
24
+ end
25
+ json.cache_if!(can_cache, can_cache ? association_cache_key(record, inc, subinc) : nil) do
26
+ value = record.send(inc)
27
+ if value.nil?
22
28
  json.set! inc, nil
23
29
  else
24
- partial = model_partial(record.send(inc))
30
+ partial = model_partial(value)
25
31
  json.set! inc do
26
- json.partial! partial, partial.split('/').last.to_sym => record.send(inc), includes: subinc
32
+ json.partial! partial, partial.split('/').last.to_sym => value, includes: subinc
27
33
  end
28
34
  end
29
35
  end
30
-
36
+
31
37
  else
32
-
33
- if record.send(inc).nil?
34
- json.set! inc, nil
35
- elsif record.send(inc).is_a?(ActiveModel::Model)
36
- json.set! inc do
37
- partial = model_partial(record.send(inc))
38
- json.partial! partial, partial.split('/').last.to_sym => record.send(inc), includes: subinc
38
+ if record.respond_to?(inc)
39
+ value = record.send(inc)
40
+ if value.nil?
41
+ json.set! inc, nil
42
+ elsif value.is_a?(ActiveModel::Model)
43
+ json.set! inc do
44
+ partial = model_partial(value)
45
+ json.partial! partial, partial.split('/').last.to_sym => value, includes: subinc
46
+ end
47
+ else
48
+ json.set! inc, value.as_json
39
49
  end
40
- else
41
- # TODO: Test
42
- json.set! inc, record.send(inc).as_json
43
50
  end
44
-
45
51
  end
46
52
 
47
53
  end
48
54
 
49
55
  if !record.errors.blank?
50
- json.set! 'errors', record.errors.to_hash
56
+ errs = record.errors.to_hash
57
+ errs.default_proc = nil
58
+ json.set! 'errors', errs
51
59
  end
@@ -1 +1,12 @@
1
- json.array! instance_variable_get("@#{model.model_name.plural}"), partial: model_partial(model), as: model_partial(model).split('/').last, includes: includes
1
+ if !includes.empty? && can_cache?(model, includes)
2
+ partial = model_partial(model)
3
+ record_name = partial.split('/').last.to_sym
4
+ locals = { record_name => nil, :includes => includes}
5
+
6
+ json.cache_collection! instance_variable_get("@#{model.model_name.plural}"), key: proc {|record| cache_key(record, includes) } do |record|
7
+ locals[record_name] = record
8
+ json.partial! partial, locals
9
+ end
10
+ else
11
+ json.array! instance_variable_get("@#{model.model_name.plural}"), partial: model_partial(model), as: model_partial(model).split('/').last, includes: includes
12
+ end
@@ -0,0 +1 @@
1
+ json.partial! model_partial(model), model_partial(model).split('/').last.to_sym => instance_variable_get("@#{model.model_name.singular}"), includes: includes
@@ -17,7 +17,7 @@ mapping = {
17
17
  'double precision' => 'decimal',
18
18
  'ltree' => 'string',
19
19
  'boolean' => 'boolean',
20
- 'geometry' => 'hash'
20
+ 'geometry' => 'ewkb'
21
21
  }
22
22
 
23
23
  model.columns.each do |column|
@@ -1,2 +1 @@
1
1
  json.partial! model_partial(model), model_partial(model).split('/').last.to_sym => instance_variable_get("@#{model.model_name.singular}"), includes: includes
2
-
data/lib/standard_api.rb CHANGED
@@ -15,5 +15,5 @@ require 'standard_api/orders'
15
15
  require 'standard_api/includes'
16
16
  require 'standard_api/controller'
17
17
  require 'standard_api/helpers'
18
+ require 'standard_api/route_helpers'
18
19
  require 'standard_api/railtie'
19
-
metadata CHANGED
@@ -1,71 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standardapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.23
4
+ version: 5.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Bracy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-22 00:00:00.000000000 Z
11
+ date: 2016-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord-sort
14
+ name: rails
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: 5.0.0.rc1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.0'
26
+ version: 5.0.0.rc1
27
27
  - !ruby/object:Gem::Dependency
28
- name: activerecord-filter
28
+ name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.0'
33
+ version: 5.0.0.rc1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.0'
40
+ version: 5.0.0.rc1
41
41
  - !ruby/object:Gem::Dependency
42
- name: rails
42
+ name: actionpack
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '4.2'
47
+ version: 5.0.0.rc1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '4.2'
54
+ version: 5.0.0.rc1
55
55
  - !ruby/object:Gem::Dependency
56
- name: activesupport
56
+ name: activerecord-sort
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 5.0.0.rc1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 5.0.0.rc1
69
+ - !ruby/object:Gem::Dependency
70
+ name: activerecord-filter
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '4.2'
75
+ version: 5.0.0.rc1
62
76
  type: :runtime
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '4.2'
82
+ version: 5.0.0.rc1
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: jbuilder
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +206,20 @@ dependencies:
192
206
  - - ">="
193
207
  - !ruby/object:Gem::Version
194
208
  version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: mocha
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
195
223
  description: StandardAPI makes it easy to expose a query interface for your Rails
196
224
  models
197
225
  email:
@@ -206,18 +234,22 @@ files:
206
234
  - lib/standard_api/controller.rb
207
235
  - lib/standard_api/helpers.rb
208
236
  - lib/standard_api/includes.rb
237
+ - lib/standard_api/middleware/query_encoding.rb
209
238
  - lib/standard_api/orders.rb
210
239
  - lib/standard_api/railtie.rb
240
+ - lib/standard_api/route_helpers.rb
211
241
  - lib/standard_api/test_case.rb
212
242
  - lib/standard_api/test_case/calculate_tests.rb
213
243
  - lib/standard_api/test_case/create_tests.rb
214
244
  - lib/standard_api/test_case/destroy_tests.rb
215
245
  - lib/standard_api/test_case/index_tests.rb
246
+ - lib/standard_api/test_case/new_tests.rb
216
247
  - lib/standard_api/test_case/schema_test.rb
217
248
  - lib/standard_api/test_case/show_tests.rb
218
249
  - lib/standard_api/test_case/update_tests.rb
219
250
  - lib/standard_api/views/application/_record.json.jbuilder
220
251
  - lib/standard_api/views/application/index.json.jbuilder
252
+ - lib/standard_api/views/application/new.json.jbuilder
221
253
  - lib/standard_api/views/application/schema.json.jbuilder
222
254
  - lib/standard_api/views/application/show.json.jbuilder
223
255
  homepage: https://github.com/waratuman/standardapi
@@ -237,12 +269,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
237
269
  version: '0'
238
270
  required_rubygems_version: !ruby/object:Gem::Requirement
239
271
  requirements:
240
- - - ">="
272
+ - - ">"
241
273
  - !ruby/object:Gem::Version
242
- version: '0'
274
+ version: 1.3.1
243
275
  requirements: []
244
276
  rubyforge_project:
245
- rubygems_version: 2.4.5
277
+ rubygems_version: 2.5.1
246
278
  signing_key:
247
279
  specification_version: 4
248
280
  summary: StandardAPI makes it easy to expose a query interface for your Rails models