safrano 0.4.1 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
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