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.
- checksums.yaml +4 -4
- data/.editorconfig +35 -0
- data/.ruby-version +1 -1
- data/.travis.yml +0 -2
- data/Guardfile +1 -1
- data/README.md +13 -8
- data/Rakefile +10 -0
- data/config.ru +3 -3
- data/index.html +1 -0
- data/jsonapionify.gemspec +13 -8
- data/lib/jsonapionify/api/action.rb +60 -50
- data/lib/jsonapionify/api/attribute.rb +13 -2
- data/lib/jsonapionify/api/base/app_builder.rb +17 -2
- data/lib/jsonapionify/api/base/class_methods.rb +33 -17
- data/lib/jsonapionify/api/base/delegation.rb +4 -1
- data/lib/jsonapionify/api/base/doc_helper.rb +13 -4
- data/lib/jsonapionify/api/base/resource_definitions.rb +13 -2
- data/lib/jsonapionify/api/base.rb +22 -6
- data/lib/jsonapionify/api/context_delegate.rb +2 -2
- data/lib/jsonapionify/api/errors.rb +7 -2
- data/lib/jsonapionify/api/errors_object.rb +1 -1
- data/lib/jsonapionify/api/header_options.rb +6 -5
- data/lib/jsonapionify/api/param_options.rb +49 -7
- data/lib/jsonapionify/api/relationship/many.rb +0 -5
- data/lib/jsonapionify/api/relationship/one.rb +10 -9
- data/lib/jsonapionify/api/relationship.rb +17 -5
- data/lib/jsonapionify/api/resource/builders.rb +39 -10
- data/lib/jsonapionify/api/resource/class_methods.rb +17 -6
- data/lib/jsonapionify/api/resource/defaults/actions.rb +0 -1
- data/lib/jsonapionify/api/resource/defaults/errors.rb +11 -11
- data/lib/jsonapionify/api/resource/defaults/options.rb +53 -0
- data/lib/jsonapionify/api/resource/defaults/params.rb +9 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +17 -11
- data/lib/jsonapionify/api/resource/definitions/actions.rb +51 -45
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +18 -0
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +183 -53
- data/lib/jsonapionify/api/resource/definitions/params.rb +43 -12
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +1 -67
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +2 -13
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +71 -58
- data/lib/jsonapionify/api/resource/error_handling.rb +2 -2
- data/lib/jsonapionify/api/resource/includer.rb +6 -0
- data/lib/jsonapionify/api/resource.rb +14 -3
- data/lib/jsonapionify/api/response.rb +2 -2
- data/lib/jsonapionify/api/server/mock_response.rb +2 -2
- data/lib/jsonapionify/api/server/request.rb +11 -7
- data/lib/jsonapionify/api/server.rb +1 -1
- data/lib/jsonapionify/api/sort_field.rb +59 -0
- data/lib/jsonapionify/api/sort_field_set.rb +36 -0
- data/lib/jsonapionify/callbacks.rb +3 -3
- data/lib/jsonapionify/continuation.rb +1 -0
- data/lib/jsonapionify/deep_sort_collection.rb +22 -0
- data/lib/jsonapionify/documentation/template.erb +196 -77
- data/lib/jsonapionify/documentation.rb +9 -9
- data/lib/jsonapionify/indented_string.rb +1 -0
- data/lib/jsonapionify/inherited_attributes.rb +4 -3
- data/lib/jsonapionify/structure/collections/base.rb +2 -1
- data/lib/jsonapionify/structure/helpers/errors.rb +1 -1
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +2 -1
- data/lib/jsonapionify/structure/helpers/validations.rb +2 -1
- data/lib/jsonapionify/structure/objects/base.rb +4 -3
- data/lib/jsonapionify/structure/objects/top_level.rb +1 -1
- data/lib/jsonapionify/types/boolean_type.rb +2 -2
- data/lib/jsonapionify/types/date_string_type.rb +1 -1
- data/lib/jsonapionify/types/time_string_type.rb +1 -1
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +16 -2
- metadata +69 -10
- data/fixtures/documentation.json +0 -364
- data/lib/jsonapionify/api/resource/http.rb +0 -11
- data/lib/jsonapionify/enumerable_observer.rb +0 -91
- 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
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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 `#{
|
29
|
+
detail "resource `#{type}` does not have field: #{field.name}"
|
57
30
|
end
|
31
|
+
next
|
58
32
|
end
|
59
33
|
end
|
60
|
-
|
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
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
81
|
-
|
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
|
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 {
|
@@ -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.
|
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['
|
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
|
9
|
-
@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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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|
|
@@ -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]
|
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
|
@@ -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
|