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.
- 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
|