standardapi 1.0.23 → 5.0.0.rc1

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