jsonapionify 0.0.1.pre → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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