safrano 0.4.1 → 0.4.6
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/lib/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +15 -10
- data/lib/odata/batch.rb +15 -13
- data/lib/odata/collection.rb +144 -535
- data/lib/odata/collection_filter.rb +47 -40
- data/lib/odata/collection_media.rb +155 -99
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +36 -34
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +183 -216
- data/lib/odata/error.rb +195 -31
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +44 -36
- data/lib/odata/filter/sequel.rb +136 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +113 -63
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +639 -0
- data/lib/odata/navigation_attribute.rb +44 -61
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +54 -0
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +20 -10
- data/lib/safrano.rb +17 -37
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +29 -104
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +39 -43
- data/lib/safrano/rack_app.rb +68 -67
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +102 -51
- data/lib/safrano/response.rb +5 -3
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +274 -219
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +34 -11
data/lib/odata/walker.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'rexml/document'
|
3
|
-
require 'safrano
|
5
|
+
require 'safrano'
|
4
6
|
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# handle navigation in the Datamodel tree of entities/attributes
|
7
9
|
# input is the url path. Url parameters ($filter etc...) are NOT handled here
|
8
10
|
# This uses a state transition algorithm
|
@@ -28,8 +30,12 @@ module OData
|
|
28
30
|
# are $links requested ?
|
29
31
|
attr_reader :do_links
|
30
32
|
|
33
|
+
NIL_SERVICE_FATAL = 'Walker is called with a nil service'
|
34
|
+
EMPTYSTR = ''
|
35
|
+
SLASH = '/'
|
36
|
+
|
31
37
|
def initialize(service, path, content_id_refs = nil)
|
32
|
-
raise
|
38
|
+
raise NIL_SERVICE_FATAL unless service
|
33
39
|
|
34
40
|
path = URI.decode_www_form_component(path)
|
35
41
|
@context = service
|
@@ -40,10 +46,9 @@ module OData
|
|
40
46
|
@path_start = @path_remain = if service
|
41
47
|
unprefixed(service.xpath_prefix, path)
|
42
48
|
else # This is for batch function
|
43
|
-
|
44
49
|
path
|
45
50
|
end
|
46
|
-
@path_done =
|
51
|
+
@path_done = String.new
|
47
52
|
@status = :start
|
48
53
|
@end_context = nil
|
49
54
|
@do_count = nil
|
@@ -51,12 +56,12 @@ module OData
|
|
51
56
|
end
|
52
57
|
|
53
58
|
def unprefixed(prefix, path)
|
54
|
-
if (prefix ==
|
59
|
+
if (prefix == EMPTYSTR) || (prefix == SLASH)
|
55
60
|
path
|
56
61
|
else
|
57
62
|
# path.sub!(/\A#{prefix}/, '')
|
58
63
|
# TODO check
|
59
|
-
path.sub(/\A#{prefix}/,
|
64
|
+
path.sub(/\A#{prefix}/, EMPTYSTR)
|
60
65
|
end
|
61
66
|
end
|
62
67
|
|
@@ -72,6 +77,7 @@ module OData
|
|
72
77
|
valid_tr = @context.allowed_transitions.select do |t|
|
73
78
|
t.do_match(@path_remain)
|
74
79
|
end
|
80
|
+
|
75
81
|
# this is a very fragile and obscure but required hack (wanted: a
|
76
82
|
# better one) to make attributes that are substrings of each other
|
77
83
|
# work well
|
@@ -93,13 +99,13 @@ module OData
|
|
93
99
|
@context = nil
|
94
100
|
@status = :error
|
95
101
|
# TODO: more appropriate error handling
|
96
|
-
@error =
|
102
|
+
@error = Safrano::ErrorNotFound
|
97
103
|
end
|
98
104
|
else
|
99
105
|
@context = nil
|
100
106
|
@status = :error
|
101
107
|
# TODO: more appropriate error handling
|
102
|
-
@error =
|
108
|
+
@error = Safrano::ErrorNotFound
|
103
109
|
end
|
104
110
|
end
|
105
111
|
|
@@ -149,7 +155,7 @@ module OData
|
|
149
155
|
else
|
150
156
|
@context = nil
|
151
157
|
@status = :error
|
152
|
-
@error =
|
158
|
+
@error = Safrano::ErrorNotFoundSegment.new(@path_remain)
|
153
159
|
end
|
154
160
|
end
|
155
161
|
# TODO: shouldnt we raise an error here if @status != :end ?
|
@@ -157,5 +163,9 @@ module OData
|
|
157
163
|
|
158
164
|
@end_context = @contexts.size >= 2 ? @contexts[-2] : @contexts[1]
|
159
165
|
end
|
166
|
+
|
167
|
+
def finalize
|
168
|
+
(@status == :end) ? Contract.valid(@end_context) : @error
|
169
|
+
end
|
160
170
|
end
|
161
171
|
end
|
data/lib/safrano.rb
CHANGED
@@ -1,42 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
3
|
require 'json'
|
3
4
|
require 'rexml/document'
|
4
|
-
require_relative 'safrano/
|
5
|
-
require_relative 'safrano/
|
6
|
-
require_relative '
|
7
|
-
require_relative '
|
8
|
-
require_relative '
|
9
|
-
require_relative '
|
10
|
-
require_relative '
|
11
|
-
require_relative 'odata/
|
5
|
+
require_relative 'safrano/version'
|
6
|
+
require_relative 'safrano/deprecation'
|
7
|
+
require_relative 'safrano/core_ext'
|
8
|
+
require_relative 'safrano/contract'
|
9
|
+
require_relative 'safrano/multipart'
|
10
|
+
require_relative 'safrano/core'
|
11
|
+
require_relative 'odata/transition'
|
12
|
+
require_relative 'odata/edm/primitive_types'
|
13
|
+
require_relative 'odata/entity'
|
14
|
+
require_relative 'odata/attribute'
|
15
|
+
require_relative 'odata/navigation_attribute'
|
16
|
+
require_relative 'odata/model_ext'
|
17
|
+
require_relative 'safrano/service'
|
18
|
+
require_relative 'odata/walker'
|
12
19
|
require 'sequel'
|
13
|
-
require_relative 'safrano/sequel_join_by_paths
|
20
|
+
require_relative 'safrano/sequel_join_by_paths'
|
14
21
|
require_relative 'safrano/rack_app'
|
15
|
-
require_relative 'safrano/
|
16
|
-
require_relative 'safrano/version'
|
17
|
-
|
18
|
-
# picked from activsupport; needed for ruby < 2.5
|
19
|
-
# Destructively converts all keys using the +block+ operations.
|
20
|
-
# Same as +transform_keys+ but modifies +self+.
|
21
|
-
class Hash
|
22
|
-
def transform_keys!
|
23
|
-
keys.each do |key|
|
24
|
-
self[yield(key)] = delete(key)
|
25
|
-
end
|
26
|
-
self
|
27
|
-
end unless method_defined? :transform_keys!
|
28
|
-
|
29
|
-
def symbolize_keys!
|
30
|
-
transform_keys! { |key| key.to_sym rescue key }
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# needed for ruby < 2.5
|
35
|
-
class Dir
|
36
|
-
def self.each_child(dir)
|
37
|
-
Dir.foreach(dir) {|x|
|
38
|
-
next if ( ( x == '.' ) or ( x == '..' ) )
|
39
|
-
yield x
|
40
|
-
}
|
41
|
-
end unless respond_to? :each_child
|
42
|
-
end
|
22
|
+
require_relative 'safrano/rack_builder'
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Safrano
|
4
|
+
# Design: the Invalid module and the Valid class share exactly the same
|
5
|
+
# methods (ie. interface) so methods returning such objects can be
|
6
|
+
# post-processed in a declarative way
|
7
|
+
# Example:
|
8
|
+
# something.do_stuff(param).tap_valid{|valid_result| ... }
|
9
|
+
# .tap_error{|error| ... }
|
10
|
+
|
11
|
+
module Contract
|
12
|
+
# represents a invalid result, ie. an error
|
13
|
+
# this shall be included/extended to our Error classes thus
|
14
|
+
# automagically making them contract/flow enabled
|
15
|
+
|
16
|
+
# All tap_valid* handlers are not executed
|
17
|
+
# tap_error* handlers are executed
|
18
|
+
module Invalid
|
19
|
+
def tap_error
|
20
|
+
yield self
|
21
|
+
self # allow chaining
|
22
|
+
end
|
23
|
+
|
24
|
+
def tap_valid
|
25
|
+
self # allow chaining
|
26
|
+
end
|
27
|
+
|
28
|
+
def if_valid
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def if_error
|
33
|
+
yield self ## return this
|
34
|
+
end
|
35
|
+
|
36
|
+
def if_valid_collect
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def map_result!
|
41
|
+
self # allow chaining
|
42
|
+
end
|
43
|
+
|
44
|
+
def collect_result!
|
45
|
+
self # allow chaining
|
46
|
+
end
|
47
|
+
|
48
|
+
def error
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def result
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Error
|
58
|
+
include Invalid
|
59
|
+
end
|
60
|
+
|
61
|
+
# represents a valid result.
|
62
|
+
# All tap_valid* handlers are executed
|
63
|
+
# tap_error* handlers are not executed
|
64
|
+
class Valid
|
65
|
+
def initialize(result)
|
66
|
+
@result = result
|
67
|
+
end
|
68
|
+
|
69
|
+
def tap_error
|
70
|
+
self # allow chaining
|
71
|
+
end
|
72
|
+
|
73
|
+
def tap_valid
|
74
|
+
yield @result
|
75
|
+
self # allow chaining
|
76
|
+
end
|
77
|
+
|
78
|
+
def if_valid
|
79
|
+
yield @result ## return this
|
80
|
+
end
|
81
|
+
|
82
|
+
def if_error
|
83
|
+
self # allow chaining
|
84
|
+
end
|
85
|
+
|
86
|
+
def if_valid_collect
|
87
|
+
yield(*@result) ## return this
|
88
|
+
end
|
89
|
+
|
90
|
+
def map_result!
|
91
|
+
@result = yield @result
|
92
|
+
self # allow chaining
|
93
|
+
end
|
94
|
+
|
95
|
+
def collect_result!
|
96
|
+
@result = yield(*@result)
|
97
|
+
self # allow chaining
|
98
|
+
end
|
99
|
+
|
100
|
+
def error
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def result
|
105
|
+
@result
|
106
|
+
end
|
107
|
+
end # class Valid
|
108
|
+
|
109
|
+
def self.valid(result)
|
110
|
+
Contract::Valid.new(result)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.and(*contracts)
|
114
|
+
# pick the first error if any
|
115
|
+
if (ff = contracts.find(&:error))
|
116
|
+
return ff
|
117
|
+
end
|
118
|
+
|
119
|
+
# return a new one with @result = list of the contracts's results
|
120
|
+
# usually this then be reduced again with #collect_result! or # #if_valid_collect methods
|
121
|
+
valid(contracts.map(&:result))
|
122
|
+
end
|
123
|
+
|
124
|
+
# shortcut for Contract.and(*contracts).collect_result!
|
125
|
+
def self.collect_result!(*contracts)
|
126
|
+
# pick the first error if any
|
127
|
+
if (ff = contracts.find(&:error))
|
128
|
+
return ff
|
129
|
+
end
|
130
|
+
|
131
|
+
# return a new one with @result = yield(*list of the contracts's results)
|
132
|
+
valid(yield(*contracts.map(&:result)))
|
133
|
+
end
|
134
|
+
|
135
|
+
# generic success return, when return value does not matter
|
136
|
+
# but usefull for control-flow
|
137
|
+
OK = Contract.valid(nil).freeze
|
138
|
+
# generic error return, when error value does not matter
|
139
|
+
# but usefull for control-flow
|
140
|
+
NOT_OK = Error.new.freeze
|
141
|
+
NOK = NOT_OK # it's shorter
|
142
|
+
end # Contract
|
143
|
+
end
|
data/lib/safrano/core.rb
CHANGED
@@ -1,118 +1,43 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# our main namespace
|
4
|
-
module
|
4
|
+
module Safrano
|
5
|
+
# frozen empty Array/Hash to reduce unncecessary object creation
|
6
|
+
EMPTY_ARRAY = [].freeze
|
7
|
+
EMPTY_HASH = {}.freeze
|
8
|
+
EMPTY_HASH_IN_ARY = [EMPTY_HASH].freeze
|
9
|
+
EMPTY_STRING = ''
|
10
|
+
ARY_204_EMPTY_HASH_ARY = [204, EMPTY_HASH, EMPTY_ARRAY].freeze
|
11
|
+
SPACE = ' '
|
12
|
+
COMMA = ','
|
13
|
+
|
5
14
|
# some prominent constants... probably already defined elsewhere eg in Rack
|
6
15
|
# but lets KISS
|
7
|
-
CONTENT_TYPE = 'Content-Type'
|
8
|
-
CTT_TYPE_LC = 'content-type'
|
9
|
-
TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'
|
10
|
-
APPJSON = 'application/json'
|
11
|
-
APPXML = 'application/xml'
|
12
|
-
MP_MIXED = 'multipart/mixed'
|
13
|
-
APPXML_UTF8 = 'application/xml;charset=utf-8'
|
14
|
-
APPATOMXML_UTF8 = 'application/atomsvc+xml;charset=utf-8'
|
15
|
-
APPJSON_UTF8 = 'application/json;charset=utf-8'
|
16
|
+
CONTENT_TYPE = 'Content-Type'
|
17
|
+
CTT_TYPE_LC = 'content-type'
|
18
|
+
TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'
|
19
|
+
APPJSON = 'application/json'
|
20
|
+
APPXML = 'application/xml'
|
21
|
+
MP_MIXED = 'multipart/mixed'
|
22
|
+
APPXML_UTF8 = 'application/xml;charset=utf-8'
|
23
|
+
APPATOMXML_UTF8 = 'application/atomsvc+xml;charset=utf-8'
|
24
|
+
APPJSON_UTF8 = 'application/json;charset=utf-8'
|
16
25
|
|
17
26
|
CT_JSON = { CONTENT_TYPE => APPJSON_UTF8 }.freeze
|
18
27
|
CT_TEXT = { CONTENT_TYPE => TEXTPLAIN_UTF8 }.freeze
|
19
28
|
CT_ATOMXML = { CONTENT_TYPE => APPATOMXML_UTF8 }.freeze
|
20
29
|
CT_APPXML = { CONTENT_TYPE => APPXML_UTF8 }.freeze
|
21
30
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
# TODO: use Sequel GENERIC_TYPES: -->
|
28
|
-
# Constants
|
29
|
-
# GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime
|
30
|
-
# Time File TrueClass FalseClass'.freeze
|
31
|
-
# Classes specifying generic types that Sequel will convert to
|
32
|
-
# database-specific types.
|
33
|
-
DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
|
31
|
+
module NavigationInfo
|
32
|
+
attr_reader :nav_parent
|
33
|
+
attr_reader :navattr_reflection
|
34
|
+
attr_reader :nav_name
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
when 'TEXT', 'STRING'
|
41
|
-
'Edm.String'
|
42
|
-
else
|
43
|
-
'Edm.String' if DB_TYPE_STRING_RGX =~ db_type.upcase
|
36
|
+
def set_relation_info(parent, name)
|
37
|
+
@nav_parent = parent
|
38
|
+
@nav_name = name
|
39
|
+
@navattr_reflection = parent.class.association_reflections[name.to_sym]
|
40
|
+
@nav_klass = @navattr_reflection[:class_name].constantize
|
44
41
|
end
|
45
42
|
end
|
46
43
|
end
|
47
|
-
|
48
|
-
module REXML
|
49
|
-
# some small extensions
|
50
|
-
class Document
|
51
|
-
def to_pretty_xml
|
52
|
-
formatter = REXML::Formatters::Pretty.new(2)
|
53
|
-
formatter.compact = true
|
54
|
-
strio = ''
|
55
|
-
formatter.write(root, strio)
|
56
|
-
strio
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# Core
|
62
|
-
module Safrano
|
63
|
-
# represents a state transition when navigating/parsing the url path
|
64
|
-
# from left to right
|
65
|
-
class Transition < Regexp
|
66
|
-
attr_accessor :trans
|
67
|
-
attr_accessor :match_result
|
68
|
-
attr_accessor :rgx
|
69
|
-
attr_reader :remain_idx
|
70
|
-
def initialize(arg, trans: nil, remain_idx: 2)
|
71
|
-
@rgx = if arg.respond_to? :each_char
|
72
|
-
Regexp.new(arg)
|
73
|
-
else
|
74
|
-
arg
|
75
|
-
end
|
76
|
-
@trans = trans
|
77
|
-
@remain_idx = remain_idx
|
78
|
-
end
|
79
|
-
|
80
|
-
def do_match(str)
|
81
|
-
@match_result = @rgx.match(str)
|
82
|
-
end
|
83
|
-
|
84
|
-
# remain_idx is the index of the last match-data. ususally its 2
|
85
|
-
# but can be overidden
|
86
|
-
def path_remain
|
87
|
-
@match_result[@remain_idx] if @match_result && @match_result[@remain_idx]
|
88
|
-
end
|
89
|
-
|
90
|
-
def path_done
|
91
|
-
if @match_result
|
92
|
-
@match_result[1] || ''
|
93
|
-
else
|
94
|
-
''
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def do_transition(ctx)
|
99
|
-
ctx.method(@trans).call(@match_result)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
|
104
|
-
TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
|
105
|
-
trans: 'transition_metadata')
|
106
|
-
TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
|
107
|
-
trans: 'transition_batch')
|
108
|
-
TransitionContentId = Transition.new('\A(\/\$(\d+))(.*)',
|
109
|
-
trans: 'transition_content_id',
|
110
|
-
remain_idx: 3)
|
111
|
-
TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
|
112
|
-
trans: 'transition_count')
|
113
|
-
TransitionValue = Transition.new('(\A\/\$value)(.*)\z',
|
114
|
-
trans: 'transition_value')
|
115
|
-
TransitionLinks = Transition.new('(\A\/\$links)(.*)\z',
|
116
|
-
trans: 'transition_links')
|
117
|
-
attr_accessor :allowed_transitions
|
118
|
-
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Dir.glob(File.expand_path('../core_ext/*.rb', __dir__)).sort.each do |path|
|
4
|
+
require path
|
5
|
+
end
|
6
|
+
|
7
|
+
# small helper method
|
8
|
+
# http://stackoverflow.com/
|
9
|
+
# questions/24980295/strictly-convert-string-to-integer-or-nil
|
10
|
+
def number_or_nil(str)
|
11
|
+
num = str.to_i
|
12
|
+
num if num.to_s == str
|
13
|
+
end
|