safrano 0.4.0 → 0.4.5

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 +145 -74
  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 +151 -197
  23. data/lib/odata/error.rb +175 -32
  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 +637 -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 +19 -11
  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 +264 -220
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -12
@@ -1,10 +1,10 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
4
  require 'rexml/document'
5
- require 'safrano.rb'
5
+ require 'safrano'
6
6
 
7
- module OData
7
+ module Safrano
8
8
  # handle navigation in the Datamodel tree of entities/attributes
9
9
  # input is the url path. Url parameters ($filter etc...) are NOT handled here
10
10
  # This uses a state transition algorithm
@@ -30,8 +30,12 @@ module OData
30
30
  # are $links requested ?
31
31
  attr_reader :do_links
32
32
 
33
+ NIL_SERVICE_FATAL = 'Walker is called with a nil service'
34
+ EMPTYSTR = ''
35
+ SLASH = '/'
36
+
33
37
  def initialize(service, path, content_id_refs = nil)
34
- raise 'Walker is called with a nil service' unless service
38
+ raise NIL_SERVICE_FATAL unless service
35
39
 
36
40
  path = URI.decode_www_form_component(path)
37
41
  @context = service
@@ -42,10 +46,9 @@ module OData
42
46
  @path_start = @path_remain = if service
43
47
  unprefixed(service.xpath_prefix, path)
44
48
  else # This is for batch function
45
-
46
49
  path
47
50
  end
48
- @path_done = ''
51
+ @path_done = String.new
49
52
  @status = :start
50
53
  @end_context = nil
51
54
  @do_count = nil
@@ -53,12 +56,12 @@ module OData
53
56
  end
54
57
 
55
58
  def unprefixed(prefix, path)
56
- if (prefix == '') || (prefix == '/')
59
+ if (prefix == EMPTYSTR) || (prefix == SLASH)
57
60
  path
58
61
  else
59
62
  # path.sub!(/\A#{prefix}/, '')
60
63
  # TODO check
61
- path.sub(/\A#{prefix}/, '')
64
+ path.sub(/\A#{prefix}/, EMPTYSTR)
62
65
  end
63
66
  end
64
67
 
@@ -74,6 +77,7 @@ module OData
74
77
  valid_tr = @context.allowed_transitions.select do |t|
75
78
  t.do_match(@path_remain)
76
79
  end
80
+
77
81
  # this is a very fragile and obscure but required hack (wanted: a
78
82
  # better one) to make attributes that are substrings of each other
79
83
  # work well
@@ -95,13 +99,13 @@ module OData
95
99
  @context = nil
96
100
  @status = :error
97
101
  # TODO: more appropriate error handling
98
- @error = OData::ErrorNotFound
102
+ @error = Safrano::ErrorNotFound
99
103
  end
100
104
  else
101
105
  @context = nil
102
106
  @status = :error
103
107
  # TODO: more appropriate error handling
104
- @error = OData::ErrorNotFound
108
+ @error = Safrano::ErrorNotFound
105
109
  end
106
110
  end
107
111
 
@@ -151,7 +155,7 @@ module OData
151
155
  else
152
156
  @context = nil
153
157
  @status = :error
154
- @error = OData::ErrorNotFound
158
+ @error = Safrano::ErrorNotFoundSegment.new(@path_remain)
155
159
  end
156
160
  end
157
161
  # TODO: shouldnt we raise an error here if @status != :end ?
@@ -159,5 +163,9 @@ module OData
159
163
 
160
164
  @end_context = @contexts.size >= 2 ? @contexts[-2] : @contexts[1]
161
165
  end
166
+
167
+ def finalize
168
+ (@status == :end) ? Contract.valid(@end_context) : @error
169
+ end
162
170
  end
163
171
  end
@@ -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
@@ -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