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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +155 -99
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +183 -216
  23. data/lib/odata/error.rb +195 -31
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +274 -219
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. 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.rb'
5
+ require 'safrano'
4
6
 
5
- module OData
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 'Walker is called with a nil service' unless service
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 == '') || (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 = OData::ErrorNotFound
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 = OData::ErrorNotFound
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 = OData::ErrorNotFound
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/multipart.rb'
5
- require_relative 'safrano/core.rb'
6
- require_relative 'odata/entity.rb'
7
- require_relative 'odata/attribute.rb'
8
- require_relative 'odata/navigation_attribute.rb'
9
- require_relative 'odata/collection.rb'
10
- require_relative 'safrano/service.rb'
11
- require_relative 'odata/walker.rb'
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.rb'
20
+ require_relative 'safrano/sequel_join_by_paths'
14
21
  require_relative 'safrano/rack_app'
15
- require_relative 'safrano/odata_rack_builder'
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
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  # our main namespace
4
- module OData
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'.freeze
8
- CTT_TYPE_LC = 'content-type'.freeze
9
- TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'.freeze
10
- APPJSON = 'application/json'.freeze
11
- APPXML = 'application/xml'.freeze
12
- MP_MIXED = 'multipart/mixed'.freeze
13
- APPXML_UTF8 = 'application/xml;charset=utf-8'.freeze
14
- APPATOMXML_UTF8 = 'application/atomsvc+xml;charset=utf-8'.freeze
15
- APPJSON_UTF8 = 'application/json;charset=utf-8'.freeze
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
- # Type mapping DB --> Edm
23
- # TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
24
- # "STRING" => "Edm.String"}
25
- # Todo: complete mapping... this is just for the most common ones
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
- # TODO... complete; used in $metadata
36
- def self.get_edm_type(db_type:)
37
- case db_type.upcase
38
- when 'INTEGER'
39
- 'Edm.Int32'
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