safrano 0.3.3 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odata/attribute.rb +9 -8
  3. data/lib/odata/batch.rb +8 -8
  4. data/lib/odata/collection.rb +239 -92
  5. data/lib/odata/collection_filter.rb +40 -9
  6. data/lib/odata/collection_media.rb +159 -28
  7. data/lib/odata/collection_order.rb +46 -36
  8. data/lib/odata/common_logger.rb +37 -12
  9. data/lib/odata/entity.rb +188 -99
  10. data/lib/odata/error.rb +60 -12
  11. data/lib/odata/expand.rb +123 -0
  12. data/lib/odata/filter/base.rb +66 -0
  13. data/lib/odata/filter/error.rb +33 -0
  14. data/lib/odata/filter/parse.rb +6 -12
  15. data/lib/odata/filter/sequel.rb +42 -29
  16. data/lib/odata/filter/sequel_function_adapter.rb +147 -0
  17. data/lib/odata/filter/token.rb +5 -1
  18. data/lib/odata/filter/tree.rb +45 -29
  19. data/lib/odata/navigation_attribute.rb +60 -27
  20. data/lib/odata/relations.rb +2 -2
  21. data/lib/odata/select.rb +42 -0
  22. data/lib/odata/url_parameters.rb +51 -36
  23. data/lib/odata/walker.rb +6 -6
  24. data/lib/safrano.rb +23 -13
  25. data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
  26. data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
  27. data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
  28. data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
  29. data/lib/{request.rb → safrano/request.rb} +8 -14
  30. data/lib/{response.rb → safrano/response.rb} +1 -2
  31. data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
  32. data/lib/{service.rb → safrano/service.rb} +162 -131
  33. data/lib/safrano/version.rb +3 -0
  34. data/lib/sequel/plugins/join_by_paths.rb +11 -10
  35. metadata +33 -16
  36. data/lib/version.rb +0 -4
@@ -37,25 +37,56 @@ end
37
37
 
38
38
  # filter base class and subclass in our OData namespace
39
39
  module OData
40
+ class FilterBase
41
+ # re-useable empty filtering (idempotent)
42
+ EmptyFilter = new
43
+
44
+ def self.factory(filterstr)
45
+ filterstr.nil? ? EmptyFilter : FilterByParse.new(filterstr)
46
+ end
47
+
48
+ def apply_to_dataset(dtcx)
49
+ dtcx
50
+ end
51
+
52
+ # finalize
53
+ def finalize(jh) end
54
+
55
+ def empty?
56
+ true
57
+ end
58
+
59
+ def parse_error?
60
+ false
61
+ end
62
+ end
40
63
  # should handle everything by parsing
41
- class FilterByParse
42
- def initialize(filterstr, jh)
64
+ class FilterByParse < FilterBase
65
+ attr_reader :filterstr
66
+ def initialize(filterstr)
43
67
  @filterstr = filterstr.dup
44
68
  @ast = OData::Filter::Parser.new(@filterstr).build
45
- @jh = jh
46
69
  end
47
70
 
48
- def apply_to_dataset(dtcx)
49
- filtexpr = @ast.sequel_expr(@jh)
50
- dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
71
+ # this build's up the Sequel Filter Expression, and as a side effect,
72
+ # it also finalizes the join helper that we need for the start dataset join
73
+ # the join-helper is shared by the order-by object and was potentially already
74
+ # partly built on order-by object creation.
75
+ def finalize(jh)
76
+ @filtexpr = @ast.sequel_expr(jh)
51
77
  end
52
78
 
53
- def sequel_expr
54
- @ast.sequel_expr(@jh)
79
+ def apply_to_dataset(dtcx)
80
+ # normally finalize is called before, and thus @filtexpr is set
81
+ dtcx.where(@filtexpr)
55
82
  end
56
83
 
57
84
  def parse_error?
58
- @ast.kind_of? StandardError
85
+ @ast.is_a? StandardError
86
+ end
87
+
88
+ def empty?
89
+ false
59
90
  end
60
91
  end
61
92
  end
@@ -1,4 +1,5 @@
1
1
  require 'rack'
2
+ require 'fileutils'
2
3
  require_relative './navigation_attribute.rb'
3
4
 
4
5
  module OData
@@ -9,61 +10,178 @@ module OData
9
10
 
10
11
  # Simple static File/Directory based media store handler
11
12
  # similar to Rack::Static
13
+ # with a flat directory structure
12
14
  class Static < Handler
13
15
  def initialize(root: nil)
14
16
  @root = File.absolute_path(root || Dir.pwd)
15
17
  @file_server = ::Rack::File.new(@root)
16
18
  end
17
19
 
20
+ # TODO: testcase and better abs_klass_dir design
21
+ def register(klass)
22
+ abs_klass_dir = File.absolute_path(klass.type_name, @root)
23
+ FileUtils.makedirs abs_klass_dir unless Dir.exist?(abs_klass_dir)
24
+ end
25
+
18
26
  # minimal working implementation...
19
27
  # Note: @file_server works relative to @root directory
20
28
  def odata_get(request:, entity:)
21
29
  media_env = request.env.dup
22
- relpath = Dir.chdir(abs_path(entity)) do
23
- # simple design: one file per directory, and the directory
24
- # contains the media entity-id --> implicit link between the media
25
- # entity
26
- filename = Dir.glob('*').first
27
-
28
- File.join(path(entity), filename)
30
+ media_env['PATH_INFO'] = filename(entity)
31
+ fsret = @file_server.call(media_env)
32
+ if fsret.first == 200
33
+ # provide own content type as we keep it in the media entity
34
+ fsret[1]['Content-Type'] = entity.content_type
29
35
  end
30
- media_env['PATH_INFO'] = relpath
31
- @file_server.call(media_env)
36
+ fsret
32
37
  end
33
38
 
34
- # TODO perf: this can be precalculated and cached on MediaModelKlass level
39
+ # TODO: [perf] this can be precalculated and cached on MediaModelKlass level
35
40
  # and passed as argument to save_file
41
+ # eg. /@root/Photo
36
42
  def abs_klass_dir(entity)
37
43
  File.absolute_path(entity.klass_dir, @root)
38
44
  end
39
45
 
46
+ # this is relative to @root
47
+ # eg. Photo/1
48
+ def media_path(entity)
49
+ File.join(entity.klass_dir, media_directory(entity))
50
+ end
51
+
52
+ # relative to @root
53
+ # eg Photo/1/pommes-topaz.jpg
54
+ def filename(entity)
55
+ Dir.chdir(abs_path(entity)) do
56
+ # simple design: one file per directory, and the directory
57
+ # contains the media entity-id --> implicit link between the media
58
+ # entity
59
+ File.join(media_path(entity), Dir.glob('*').first)
60
+ end
61
+ end
62
+
63
+ # /@root/Photo/1
40
64
  def abs_path(entity)
41
- File.absolute_path(path(entity), @root)
65
+ File.absolute_path(media_path(entity), @root)
42
66
  end
43
67
 
44
- # this is relative to @root
45
- def path(entity)
46
- File.join(entity.klass_dir, entity.media_path_id)
68
+ # this is relative to abs_klass_dir(entity) eg to /@root/Photo
69
+ # simplest implementation is media_directory = entity.media_path_id
70
+ # --> we get a 1 level depth flat directory structure
71
+ def media_directory(entity)
72
+ entity.media_path_id
73
+ end
74
+
75
+ def in_media_directory(entity)
76
+ mpi = media_directory(entity)
77
+ Dir.mkdir mpi unless Dir.exist?(mpi)
78
+ Dir.chdir mpi do
79
+ yield
80
+ end
81
+ end
82
+
83
+ def odata_delete(entity:)
84
+ Dir.chdir(abs_klass_dir(entity)) do
85
+ in_media_directory(entity) do
86
+ Dir.glob('*').each { |oldf| File.delete(oldf) }
87
+ end
88
+ end
47
89
  end
48
90
 
49
91
  # Here as well, MVP implementation
50
92
  def save_file(data:, filename:, entity:)
51
- mpi = entity.media_path_id
52
93
  Dir.chdir(abs_klass_dir(entity)) do
53
- Dir.mkdir mpi unless Dir.exists?(mpi)
54
- Dir.chdir mpi do
94
+ in_media_directory(entity) do
95
+ filename = '1'
55
96
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
56
97
  end
57
98
  end
58
99
  end
59
100
 
101
+ # needed for having a changing media ressource "source" metadata
102
+ # after each upload, so that clients get informed about new versions
103
+ # of the same media ressource
104
+ def ressource_version(entity)
105
+ Dir.chdir(abs_klass_dir(entity)) do
106
+ in_media_directory(entity) do
107
+ Dir.glob('*').last
108
+ end
109
+ end
110
+ end
111
+
60
112
  # Here as well, MVP implementation
61
113
  def replace_file(data:, filename:, entity:)
62
- mpi = entity.media_path_id
63
114
  Dir.chdir(abs_klass_dir(entity)) do
64
- Dir.mkdir mpi unless Dir.exists?(mpi)
65
- Dir.chdir mpi do
66
- Dir.glob('*').each { |oldf| File.delete(oldf) }
115
+ in_media_directory(entity) do
116
+ version = nil
117
+ Dir.glob('*').each do |oldf|
118
+ version = oldf
119
+ File.delete(oldf)
120
+ end
121
+ filename = (version.to_i + 1).to_s
122
+ File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
123
+ end
124
+ end
125
+ end
126
+ end
127
+ # Simple static File/Directory based media store handler
128
+ # similar to Rack::Static
129
+ # with directory Tree structure
130
+
131
+ class StaticTree < Static
132
+ SEP = '/00/'.freeze
133
+ VERS = '/v'.freeze
134
+
135
+ def self.path_builder(ids)
136
+ ids.map { |id| id.to_s.chars.join('/') }.join(SEP) << VERS
137
+ end
138
+
139
+ # this is relative to abs_klass_dir(entity) eg to /@root/Photo
140
+ # tree-structure
141
+ # media_path_ids = 1 --> 1
142
+ # media_path_ids = 15 --> 1/5
143
+ # media_path_ids = 555 --> 5/5/5
144
+ # media_path_ids = 5,5,5 --> 5/00/5/00/5
145
+ # media_path_ids = 5,00,5 --> 5/00/0/0/00/5
146
+ # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
147
+ def media_directory(entity)
148
+ StaticTree.path_builder(entity.media_path_ids)
149
+ # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
150
+ end
151
+
152
+ def in_media_directory(entity)
153
+ mpi = media_directory(entity)
154
+ FileUtils.makedirs mpi unless Dir.exist?(mpi)
155
+ Dir.chdir(mpi) { yield }
156
+ end
157
+
158
+ def odata_delete(entity:)
159
+ Dir.chdir(abs_klass_dir(entity)) do
160
+ in_media_directory(entity) do
161
+ Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
162
+ end
163
+ end
164
+ end
165
+
166
+ # Here as well, MVP implementation
167
+ # def replace_file(data:, filename:, entity:)
168
+ # Dir.chdir(abs_klass_dir(entity)) do
169
+ # in_media_directory(entity) do
170
+ # Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
171
+ # File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
172
+ # end
173
+ # end
174
+ # end
175
+ # Here as well, MVP implementation
176
+ def replace_file(data:, filename:, entity:)
177
+ Dir.chdir(abs_klass_dir(entity)) do
178
+ in_media_directory(entity) do
179
+ version = nil
180
+ Dir.glob('*').each do |oldf|
181
+ version = oldf
182
+ File.delete(oldf)
183
+ end
184
+ filename = (version.to_i + 1).to_s
67
185
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
68
186
  end
69
187
  end
@@ -74,6 +192,7 @@ module OData
74
192
  # special handling for media entity
75
193
  module EntityClassMedia
76
194
  attr_reader :media_handler
195
+ attr_reader :slug_field
77
196
 
78
197
  # API method for defining the media handler
79
198
  # eg.
@@ -85,22 +204,27 @@ module OData
85
204
  @media_handler = OData::Media::Static.new
86
205
  end
87
206
 
88
- def use(klass, *args)
89
- @media_handler = klass.new(*args)
207
+ def use(klass, args)
208
+ @media_handler = klass.new(**args)
209
+ end
210
+
211
+ # API method for setting the model field mapped to SLUG on upload
212
+ def slug(inp)
213
+ @slug_field = inp
90
214
  end
91
215
 
92
216
  def api_check_media_fields
93
- unless self.db_schema.has_key?(:content_type)
94
- raise OData::API::MediaModelError, self
95
- end
217
+ raise(OData::API::MediaModelError, self) unless db_schema.key?(:content_type)
218
+
96
219
  # unless self.db_schema.has_key?(:media_src)
97
220
  # raise OData::API::MediaModelError, self
98
221
  # end
99
222
  end
100
223
 
224
+ # END API methods
225
+
101
226
  def new_media_entity(mimetype:)
102
- nh = {}
103
- nh['content_type'] = mimetype
227
+ nh = { 'content_type' => mimetype }
104
228
  new_from_hson_h(nh)
105
229
  end
106
230
 
@@ -125,6 +249,13 @@ module OData
125
249
 
126
250
  new_entity = new_media_entity(mimetype: mimetype)
127
251
 
252
+ if slug_field
253
+
254
+ new_entity.set_fields({ slug_field => filename },
255
+ data_fields,
256
+ missing: :skip)
257
+ end
258
+
128
259
  # to_one rels are create with FK data set on the parent entity
129
260
  if parent
130
261
  odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
@@ -1,18 +1,32 @@
1
1
  require 'odata/error.rb'
2
2
 
3
- # Ordering with ruby expression
4
- module OrderWithRuby
5
- # this module requires the @fn attribute to exist where it is used
6
- def fn=(fnam)
7
- @fn = fnam
8
- @fn_tab = fnam.split('/').map(&:to_sym)
9
- end
10
- end
11
-
12
3
  # all ordering related classes in our OData module
13
4
  module OData
14
5
  # base class for ordering
15
- class Order
6
+ class OrderBase
7
+ # re-useable empty ordering (idempotent)
8
+ EmptyOrder = new
9
+
10
+ # input : the OData order string
11
+ # returns a Order object that should have a apply_to(cx) method
12
+ def self.factory(orderstr, jh)
13
+ orderstr.nil? ? EmptyOrder : MultiOrder.new(orderstr, jh)
14
+ end
15
+
16
+ def empty?
17
+ true
18
+ end
19
+
20
+ def parse_error?
21
+ false
22
+ end
23
+
24
+ def apply_to_dataset(dtcx)
25
+ dtcx
26
+ end
27
+ end
28
+
29
+ class Order < OrderBase
16
30
  attr_reader :oarg
17
31
  def initialize(ostr, jh)
18
32
  ostr.strip!
@@ -21,23 +35,14 @@ module OData
21
35
  build_oarg if @orderp
22
36
  end
23
37
 
24
- class << self
25
- attr_reader :regexp
26
- end
27
-
28
- # input : the filter string
29
- # returns a filter object that should have a apply_to(cx) method
30
- def self.new_by_parse(orderstr, jh)
31
- Order.new_full_match_complexpr(orderstr, jh)
32
- end
33
-
34
- # handle with Sequel
35
- def self.new_full_match_complexpr(orderstr, jh)
36
- ComplexOrder.new(orderstr, jh)
38
+ def empty?
39
+ false
37
40
  end
38
41
 
39
42
  def apply_to_dataset(dtcx)
40
- dtcx
43
+ # Warning, we need order_append, simply order(oarg) overwrites
44
+ # previous one !
45
+ dtcx.order_append(@oarg)
41
46
  end
42
47
 
43
48
  def build_oarg
@@ -60,26 +65,31 @@ module OData
60
65
  end
61
66
 
62
67
  # complex ordering logic
63
- class ComplexOrder < Order
68
+ class MultiOrder < Order
64
69
  def initialize(orderstr, jh)
65
70
  super
66
71
  @olist = []
67
72
  @jh = jh
68
- return unless orderstr
69
-
70
- @olist = orderstr.split(',').map do |ostr|
71
- oo = Order.new(ostr, @jh)
72
- oo.oarg
73
- end
73
+ @orderstr = orderstr.dup
74
+ @olist = orderstr.split(',').map { |ostr| Order.new(ostr, @jh) }
74
75
  end
75
76
 
76
77
  def apply_to_dataset(dtcx)
77
- @olist.each { |oarg|
78
- # Warning, we need order_append, simply order(oarg) overwrites
79
- # previous one !
80
- dtcx = dtcx.order_append(oarg)
81
- }
78
+ @olist.each { |osingl| dtcx = osingl.apply_to_dataset(dtcx) }
82
79
  dtcx
83
80
  end
81
+
82
+ def parse_error?
83
+ @orderstr.split(',').each do |pord|
84
+ pord.strip!
85
+ qualfn, dir = pord.split(/\s/)
86
+ qualfn.strip!
87
+ dir.strip! if dir
88
+ return true unless @jh.start_model.attrib_path_valid? qualfn
89
+ return true unless [nil, 'asc', 'desc'].include? dir
90
+ end
91
+
92
+ false
93
+ end
84
94
  end
85
95
  end
@@ -5,21 +5,46 @@ module Rack
5
5
  super
6
6
  end
7
7
 
8
+ # Handle https://github.com/rack/rack/pull/1526
9
+ # new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
10
+
11
+ MSG_FUNC = if FORMAT.count('%') == 10
12
+ lambda { |env, length, status, began_at|
13
+ FORMAT % [
14
+ env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
15
+ env['REMOTE_USER'] || '-',
16
+ Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
17
+ env[REQUEST_METHOD],
18
+ env[SCRIPT_NAME] + env[PATH_INFO],
19
+ env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
20
+ env[SERVER_PROTOCOL],
21
+ status.to_s[0..3],
22
+ length,
23
+ Utils.clock_time - began_at
24
+ ]
25
+ }
26
+ elsif FORMAT.count('%') == 11
27
+ lambda { |env, length, status, began_at|
28
+ FORMAT % [
29
+ env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
30
+ env['REMOTE_USER'] || '-',
31
+ Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
32
+ env[REQUEST_METHOD],
33
+ env[SCRIPT_NAME],
34
+ env[PATH_INFO],
35
+ env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
36
+ env[SERVER_PROTOCOL],
37
+ status.to_s[0..3],
38
+ length,
39
+ Utils.clock_time - began_at
40
+ ]
41
+ }
42
+ end
43
+
8
44
  def batch_log(env, status, header, began_at)
9
45
  length = extract_content_length(header)
10
46
 
11
- msg = FORMAT % [
12
- env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
13
- env["REMOTE_USER"] || "-",
14
- Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
15
- env[REQUEST_METHOD],
16
- env[PATH_INFO],
17
- env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
18
- env[SERVER_PROTOCOL],
19
- status.to_s[0..3],
20
- length,
21
- Utils.clock_time - began_at
22
- ]
47
+ msg = MSG_FUNC.call(env, length, status, began_at)
23
48
 
24
49
  logger = @logger || env[RACK_ERRORS]
25
50
  # Standard library logger doesn't support write but it supports << which actually