safrano 0.3.3 → 0.4.3

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 (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