jsonapionify 0.0.1.pre → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +35 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +0 -2
  5. data/Guardfile +1 -1
  6. data/README.md +13 -8
  7. data/Rakefile +10 -0
  8. data/config.ru +3 -3
  9. data/index.html +1 -0
  10. data/jsonapionify.gemspec +13 -8
  11. data/lib/jsonapionify/api/action.rb +60 -50
  12. data/lib/jsonapionify/api/attribute.rb +13 -2
  13. data/lib/jsonapionify/api/base/app_builder.rb +17 -2
  14. data/lib/jsonapionify/api/base/class_methods.rb +33 -17
  15. data/lib/jsonapionify/api/base/delegation.rb +4 -1
  16. data/lib/jsonapionify/api/base/doc_helper.rb +13 -4
  17. data/lib/jsonapionify/api/base/resource_definitions.rb +13 -2
  18. data/lib/jsonapionify/api/base.rb +22 -6
  19. data/lib/jsonapionify/api/context_delegate.rb +2 -2
  20. data/lib/jsonapionify/api/errors.rb +7 -2
  21. data/lib/jsonapionify/api/errors_object.rb +1 -1
  22. data/lib/jsonapionify/api/header_options.rb +6 -5
  23. data/lib/jsonapionify/api/param_options.rb +49 -7
  24. data/lib/jsonapionify/api/relationship/many.rb +0 -5
  25. data/lib/jsonapionify/api/relationship/one.rb +10 -9
  26. data/lib/jsonapionify/api/relationship.rb +17 -5
  27. data/lib/jsonapionify/api/resource/builders.rb +39 -10
  28. data/lib/jsonapionify/api/resource/class_methods.rb +17 -6
  29. data/lib/jsonapionify/api/resource/defaults/actions.rb +0 -1
  30. data/lib/jsonapionify/api/resource/defaults/errors.rb +11 -11
  31. data/lib/jsonapionify/api/resource/defaults/options.rb +53 -0
  32. data/lib/jsonapionify/api/resource/defaults/params.rb +9 -0
  33. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +17 -11
  34. data/lib/jsonapionify/api/resource/definitions/actions.rb +51 -45
  35. data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
  36. data/lib/jsonapionify/api/resource/definitions/helpers.rb +18 -0
  37. data/lib/jsonapionify/api/resource/definitions/pagination.rb +183 -53
  38. data/lib/jsonapionify/api/resource/definitions/params.rb +43 -12
  39. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +1 -67
  40. data/lib/jsonapionify/api/resource/definitions/scopes.rb +2 -13
  41. data/lib/jsonapionify/api/resource/definitions/sorting.rb +71 -58
  42. data/lib/jsonapionify/api/resource/error_handling.rb +2 -2
  43. data/lib/jsonapionify/api/resource/includer.rb +6 -0
  44. data/lib/jsonapionify/api/resource.rb +14 -3
  45. data/lib/jsonapionify/api/response.rb +2 -2
  46. data/lib/jsonapionify/api/server/mock_response.rb +2 -2
  47. data/lib/jsonapionify/api/server/request.rb +11 -7
  48. data/lib/jsonapionify/api/server.rb +1 -1
  49. data/lib/jsonapionify/api/sort_field.rb +59 -0
  50. data/lib/jsonapionify/api/sort_field_set.rb +36 -0
  51. data/lib/jsonapionify/callbacks.rb +3 -3
  52. data/lib/jsonapionify/continuation.rb +1 -0
  53. data/lib/jsonapionify/deep_sort_collection.rb +22 -0
  54. data/lib/jsonapionify/documentation/template.erb +196 -77
  55. data/lib/jsonapionify/documentation.rb +9 -9
  56. data/lib/jsonapionify/indented_string.rb +1 -0
  57. data/lib/jsonapionify/inherited_attributes.rb +4 -3
  58. data/lib/jsonapionify/structure/collections/base.rb +2 -1
  59. data/lib/jsonapionify/structure/helpers/errors.rb +1 -1
  60. data/lib/jsonapionify/structure/helpers/object_defaults.rb +2 -1
  61. data/lib/jsonapionify/structure/helpers/validations.rb +2 -1
  62. data/lib/jsonapionify/structure/objects/base.rb +4 -3
  63. data/lib/jsonapionify/structure/objects/top_level.rb +1 -1
  64. data/lib/jsonapionify/types/boolean_type.rb +2 -2
  65. data/lib/jsonapionify/types/date_string_type.rb +1 -1
  66. data/lib/jsonapionify/types/time_string_type.rb +1 -1
  67. data/lib/jsonapionify/version.rb +1 -1
  68. data/lib/jsonapionify.rb +16 -2
  69. metadata +69 -10
  70. data/fixtures/documentation.json +0 -364
  71. data/lib/jsonapionify/api/resource/http.rb +0 -11
  72. data/lib/jsonapionify/enumerable_observer.rb +0 -91
  73. data/lib/jsonapionify/unstrict_proc.rb +0 -28
@@ -1,84 +1,97 @@
1
+ require 'set'
2
+
1
3
  module JSONAPIonify::Api
2
4
  module Resource::Definitions::Sorting
5
+ using JSONAPIonify::DeepSortCollection
3
6
 
4
- class SortField
5
-
6
- attr_reader :name, :order
7
-
8
- def initialize(name)
9
- if name.to_s.start_with? '-'
10
- @name = name.to_s[1..-1].to_sym
11
- @order = :desc
12
- else
13
- @name = name
14
- @order = :asc
15
- end
16
- end
17
-
18
- end
7
+ def self.extended(klass)
8
+ klass.class_eval do
9
+ inherited_hash_attribute :sorting_strategies
10
+ delegate :sort_fields_from_sort_string, to: :class
19
11
 
20
- STRATEGIES = {
21
- active_record: proc { |collection, fields|
22
- order_hash = fields.each_with_object({}) do |field, hash|
23
- hash[field.name] = field.order
24
- end
25
- collection.order order_hash
26
- },
27
- enumerable: proc { |collection, fields|
28
- fields.reverse.reduce(collection) do |o, field|
29
- result = o.sort_by(&field.name)
30
- case field.order
31
- when :asc
32
- result
33
- when :desc
34
- result.reverse
12
+ # Define Contexts
13
+ context :sorted_collection do |context|
14
+ _, block = sorting_strategies.to_a.reverse.to_h.find do |mod, _|
15
+ Object.const_defined?(mod, false) && context.collection.class <= Object.const_get(mod, false)
35
16
  end
17
+ context.reset(:sort_params)
18
+ instance_exec(context.collection, context.sort_params, context, &block)
36
19
  end
37
- }
38
- }
39
- STRATEGIES[:array] = STRATEGIES[:enumerable]
40
- DEFAULT = STRATEGIES[:enumerable]
41
-
42
- def self.extended(klass)
43
- klass.class_eval do
44
20
 
45
21
  context(:sort_params, readonly: true) do |context|
46
- should_error = false
47
- fields = context.params['sort'].to_s.split(',')
48
- fields.each_with_object([]) do |field, array|
49
- field, resource = field.split('.').map(&:to_sym).reverse
50
- if self.class <= self.class.api.resource(resource || self.class.type)
51
- if self.class.field_valid? field
52
- array << SortField.new(field)
53
- else
22
+ sort_fields_from_sort_string(context.params['sort']).tap do |fields|
23
+ should_error = false
24
+ fields.each do |field|
25
+ unless self.class.field_valid?(field.name) || field.name == id_attribute
54
26
  should_error = true
27
+ type = self.class.type
55
28
  error :sort_parameter_invalid do
56
- detail "resource `#{self.class.type}` does not have field: #{field}"
29
+ detail "resource `#{type}` does not have field: #{field.name}"
57
30
  end
31
+ next
58
32
  end
59
33
  end
60
- end.tap do
61
- raise error_exception if should_error
34
+ raise Errors::RequestError if should_error
62
35
  end
63
36
  end
37
+
38
+ define_sorting_strategy('Object') do |collection|
39
+ collection
40
+ end
41
+
42
+ define_sorting_strategy('Enumerable') do |collection, fields|
43
+ collection.to_a.deep_sort(fields.to_hash)
44
+ end
45
+
46
+ define_sorting_strategy('ActiveRecord::Relation') do |collection, fields|
47
+ collection.reorder(fields.to_hash).order(self.class.id_attribute)
48
+ end
49
+
50
+ # Configure the default sort
51
+ default_sort 'id'
52
+
64
53
  end
65
54
  end
66
55
 
67
- def sorting(strategy = nil, &block)
68
- param :sort
69
- context :sorted_collection do |context|
70
- unless (actual_block = block)
71
- actual_strategy = strategy || self.class.default_strategy
72
- actual_block = actual_strategy ? STRATEGIES[actual_strategy] : DEFAULT
56
+ def define_sorting_strategy(mod, &block)
57
+ sorting_strategies[mod.to_s] = block
58
+ end
59
+
60
+ def default_sort(options)
61
+ string =
62
+ case options
63
+ when Hash, Array
64
+ options.map do |k, v|
65
+ v.to_s.downcase == 'asc' ? "-#{k}" : k.to_s
66
+ end.join(',')
67
+ else
68
+ options.to_s
73
69
  end
74
- Object.new.instance_exec(context.collection, context.sort_params, &actual_block)
70
+ param :sort, default: string, sticky: true
71
+ end
72
+
73
+ def sort_attrs_from_sort(sort_string)
74
+ sort_attrs = sort_string.split(',').map do |a|
75
+ a == 'id' ? id_attribute.to_s : a.to_s
75
76
  end
77
+ sort_attrs.uniq
76
78
  end
77
79
 
78
- private
80
+ def sort_fields_from_sort_string(sort_string)
81
+ field_specs = sort_string.to_s.split(',')
82
+ field_specs.each_with_object(SortFieldSet.new) do |field_spec, array|
83
+ field_name, resource = field_spec.split('.').map(&:to_sym).reverse
84
+
85
+ # Skip unless this resource
86
+ next unless self <= self.api.resource(resource || type)
79
87
 
80
- def default_sort
81
- api.resource_definitions.keys.each_with_object({}) { |type, h| h[type] = [] }
88
+ # Assign Sort Fields
89
+ field = SortField.new(field_name)
90
+ field = SortField.new(id_attribute.to_s) if field.id?
91
+ array << field
92
+ end.tap do |field_set|
93
+ field_set << SortField.new(id_attribute.to_s)
94
+ end
82
95
  end
83
96
 
84
97
  end
@@ -4,7 +4,6 @@ module JSONAPIonify::Api
4
4
 
5
5
  included do
6
6
  include ActiveSupport::Rescuable
7
- context(:error_exception) { Class.new(StandardError) }
8
7
  context(:errors, readonly: true) do
9
8
  ErrorsObject.new
10
9
  end
@@ -50,7 +49,7 @@ module JSONAPIonify::Api
50
49
 
51
50
  def error_now(name, *args, &block)
52
51
  error(name, *args, &block)
53
- raise error_exception
52
+ raise Errors::RequestError
54
53
  end
55
54
 
56
55
  def set_errors(collection)
@@ -71,6 +70,7 @@ module JSONAPIonify::Api
71
70
 
72
71
  def rescued_response(exception)
73
72
  rescue_with_handler(exception) || begin
73
+ run_callbacks(:exception, exception)
74
74
  errors.evaluate(
75
75
  error_block: lookup_error(:internal_server_error),
76
76
  runtime_block: proc {
@@ -1,4 +1,10 @@
1
1
  module JSONAPIonify::Api
2
2
  module Resource::Includer
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ param :include
7
+ end
8
+
3
9
  end
4
10
  end
@@ -11,25 +11,36 @@ module JSONAPIonify::Api
11
11
  extend ClassMethods
12
12
 
13
13
  include ErrorHandling
14
+ include Includer
14
15
  include Builders
15
16
  include Defaults
16
17
 
17
18
  def self.inherited(subclass)
18
19
  super(subclass)
19
20
  subclass.class_eval do
20
- context(:api, readonly: true) { api }
21
+ context(:api, readonly: true) { self.class.api }
21
22
  context(:resource, readonly: true) { self }
22
23
  end
23
24
  end
24
25
 
25
- def self.example_instance(id=1)
26
+ def self.example_id_generator(&block)
27
+ define_singleton_method :generate_id, &block
28
+ end
29
+
30
+ def self.example_instance(index=1)
31
+ id = generate_id(index)
26
32
  OpenStruct.new.tap do |instance|
27
33
  instance.send "#{id_attribute}=", (id).to_s
28
34
  attributes.select(&:read?).each do |attribute|
29
- instance.send "#{attribute.name}=", attribute.example
35
+ instance.send "#{attribute.name}=", attribute.example(id, index)
30
36
  end
31
37
  end
32
38
  end
33
39
 
40
+ example_id_generator { |val| val }
41
+
42
+ def action_name
43
+ end
44
+
34
45
  end
35
46
  end
@@ -36,8 +36,8 @@ module JSONAPIonify::Api
36
36
  body = instance_exec(context, &response.response_block)
37
37
  Rack::Response.new.tap do |rack_response|
38
38
  rack_response.status = response.status
39
- response_headers.each { |k, v| rack_response.headers[k] = v }
40
- rack_response.headers['content-type'] = response.accept unless response.accept == '*/*'
39
+ response_headers.each { |k, v| rack_response.headers[k.split('-').map(&:capitalize).join('-')] = v }
40
+ rack_response.headers['Content-Type'] = response.accept unless response.accept == '*/*'
41
41
  rack_response.write(body) unless body.nil?
42
42
  end.finish
43
43
  end
@@ -5,8 +5,8 @@ module JSONAPIonify::Api
5
5
  attr_reader :status, :headers, :body
6
6
 
7
7
  def initialize(status, headers, body)
8
- @status = status
9
- @body = body.is_a?(Rack::BodyProxy) ? body.body : body
8
+ @status = status
9
+ @body = body.is_a?(Rack::BodyProxy) ? body.body : body
10
10
  @headers = Rack::Utils::HeaderHash.new headers
11
11
  end
12
12
 
@@ -10,13 +10,13 @@ module JSONAPIonify::Api
10
10
  end
11
11
 
12
12
  def headers
13
- Rack::Utils::HeaderHash.new(
14
- env.select do |name, _|
15
- name.start_with?('HTTP_') && !%w{HTTP_VERSION}.include?(name)
16
- end.each_with_object({}) do |(name, value), hash|
17
- hash[name[5..-1].gsub('_', '-')] = value
18
- end
19
- )
13
+ env_headers = env.select do |name, _|
14
+ name.start_with?('HTTP_') && !%w{HTTP_VERSION}.include?(name)
15
+ end.each_with_object({}) do |(name, value), hash|
16
+ hash[name[5..-1].gsub('_', '-').downcase] = value
17
+ end
18
+ env_headers['content-type'] = content_type
19
+ Rack::Utils::HeaderHash.new(env_headers)
20
20
  end
21
21
 
22
22
  def http_string
@@ -44,6 +44,10 @@ module JSONAPIonify::Api
44
44
  body.rewind
45
45
  end
46
46
 
47
+ def content_type
48
+ super.try(:split, ';').try(:first)
49
+ end
50
+
47
51
  def accept
48
52
  accepts = (headers['accept'] || '*/*').split(',')
49
53
  accepts.to_a.sort_by! do |accept|
@@ -33,7 +33,7 @@ module JSONAPIonify::Api
33
33
  def response
34
34
  @resource ? resource.process(request) : api_index
35
35
  rescue Errors::ResourceNotFound
36
- Resource::Http.process(:not_found, request)
36
+ api.http_error(:not_found, request)
37
37
  end
38
38
 
39
39
  private
@@ -0,0 +1,59 @@
1
+ module JSONAPIonify::Api
2
+ class SortField
3
+ delegate :to_s, :inspect, to: :to_hash
4
+ attr_reader :name, :order
5
+
6
+ def initialize(name)
7
+ @str = name.to_s
8
+ if name.to_s.start_with? '-'
9
+ @name = name.to_s[1..-1].to_sym
10
+ @order = :desc
11
+ else
12
+ @name = name.to_sym
13
+ @order = :asc
14
+ end
15
+ freeze
16
+ end
17
+
18
+ def ==(other)
19
+ other.class == self.class &&
20
+ other.name == self.name
21
+ end
22
+
23
+ def ===(other)
24
+ other.class == self.class &&
25
+ other.name == self.name
26
+ end
27
+
28
+ def id?
29
+ name == :id
30
+ end
31
+
32
+ def contains_operator
33
+ case @order
34
+ when :asc
35
+ :>=
36
+ when :desc
37
+ :<=
38
+ end
39
+ end
40
+
41
+ def outside_operator
42
+ case @order
43
+ when :asc
44
+ :>
45
+ when :desc
46
+ :<
47
+ end
48
+ end
49
+
50
+ def to_s
51
+ @str
52
+ end
53
+
54
+ def to_hash
55
+ { name => order }
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,36 @@
1
+ module JSONAPIonify::Api
2
+ class SortFieldSet
3
+ include Enumerable
4
+ delegate :[], :length, :to_s, :inspect, :each, to: :@list
5
+
6
+ def initialize
7
+ @list = []
8
+ freeze
9
+ end
10
+
11
+ def to_hash
12
+ map(&:to_hash).reduce(:merge)
13
+ end
14
+
15
+ def reverse
16
+ self.class.new.tap do |set|
17
+ each do |field|
18
+ name =
19
+ case field.order
20
+ when :asc
21
+ "-#{field.name}"
22
+ when :desc
23
+ "#{field.name}"
24
+ end
25
+ set << SortField.new(name)
26
+ end
27
+ end
28
+ end
29
+
30
+ def <<(field)
31
+ raise TypeError unless field.is_a? SortField
32
+ @list << field unless @list.include? field
33
+ end
34
+
35
+ end
36
+ end
@@ -13,16 +13,16 @@ module JSONAPIonify
13
13
  after: "__#{name}_after_callback_chain"
14
14
  }
15
15
  define_method chains[:main] do |*args, &block|
16
+ block ||= proc {}
16
17
  if send(chains[:before], *args) != false
17
18
  value = instance_exec(*args, &block)
18
19
  value if send(chains[:after], *args) != false
19
20
  end
20
- end
21
+ end unless method_defined? chains[:main]
21
22
 
22
23
  # Define before and after chains
23
24
  %i{after before}.each do |timing|
24
- define_method chains[timing] do |*|
25
- end
25
+ define_method chains[timing] { |*| } unless method_defined? chains[timing]
26
26
 
27
27
  define_singleton_method "#{timing}_#{name}" do |sym = nil, &outer_block|
28
28
  outer_block = (outer_block || sym).to_proc
@@ -1,3 +1,4 @@
1
+ require 'unstrict_proc'
1
2
  module JSONAPIonify
2
3
  class Continuation
3
4
  using UnstrictProc
@@ -0,0 +1,22 @@
1
+ module JSONAPIonify
2
+ module DeepSortCollection
3
+ refine Array do
4
+ def deep_sort(hash)
5
+ keys = hash.to_a
6
+ sorter = lambda do |iterator, depth = 0|
7
+ key_name, order = keys[depth]
8
+ if key_name
9
+ sorted = iterator.sort_by(&key_name)
10
+ sorted.reverse! if order == :desc
11
+ sorted.group_by(&key_name).values.map do |value|
12
+ sorter.call(value, depth + 1)
13
+ end.reduce(:+) || []
14
+ else
15
+ iterator
16
+ end
17
+ end
18
+ sorter.call(self)
19
+ end
20
+ end
21
+ end
22
+ end