safrano 0.4.3 → 0.4.4

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 +6 -2
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +136 -642
  16. data/lib/odata/collection_filter.rb +16 -40
  17. data/lib/odata/collection_media.rb +56 -37
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  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 +53 -117
  23. data/lib/odata/error.rb +142 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +4 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +33 -25
  28. data/lib/odata/filter/sequel.rb +97 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +10 -10
  31. data/lib/odata/filter/tree.rb +75 -41
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +15 -7
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +12 -94
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +25 -20
  46. data/lib/safrano/rack_app.rb +61 -62
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +95 -37
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +132 -94
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +24 -5
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './token.rb'
4
- require_relative './tree.rb'
5
- require_relative './error.rb'
3
+ require_relative './token'
4
+ require_relative './tree'
5
+ require_relative './error'
6
6
 
7
- # top level OData namespace
8
- module OData
7
+ # top level Safrano namespace
8
+ module Safrano
9
9
  # for handling $filter
10
10
  module Filter
11
11
  # Parser for $filter input
12
12
  class Parser
13
13
  include Token
14
14
  attr_reader :cursor
15
+ attr_reader :error
16
+
15
17
  def initialize(input)
16
18
  @tree = RootTree.new
17
19
  @cursor = @tree
@@ -20,10 +22,14 @@ module OData
20
22
  @binop_stack = []
21
23
  end
22
24
 
25
+ def server_error
26
+ (@error = ::Safrano::ServerError)
27
+ end
28
+
23
29
  def grow_at_cursor(child)
24
- raise 'unknown BroGrammingError' if @cursor.nil?
30
+ return server_error if @cursor.nil?
25
31
 
26
- @cursor.attach(child)
32
+ @cursor.attach(child).tap_error { |err| return (@error = err) }
27
33
  @cursor = child
28
34
  end
29
35
 
@@ -42,7 +48,7 @@ module OData
42
48
  def insert_before_cursor(node)
43
49
  left = detach_cursor
44
50
  grow_at_cursor(node)
45
- @cursor.attach(left)
51
+ @cursor.attach(left).tap_error { |err| return (@error = err) }
46
52
  end
47
53
 
48
54
  def invalid_separator_error(tok, typ)
@@ -58,12 +64,7 @@ module OData
58
64
  end
59
65
 
60
66
  def with_accepted(tok, typ)
61
- acc, err = @cursor.accept?(tok, typ)
62
- if acc
63
- yield
64
- else
65
- @error = err
66
- end
67
+ (err = @cursor.accept?(tok, typ)) ? (@error = err) : yield
67
68
  end
68
69
 
69
70
  def build
@@ -71,15 +72,19 @@ module OData
71
72
  case typ
72
73
  when :FuncTree
73
74
  with_accepted(tok, typ) { grow_at_cursor(FuncTree.new(tok)) }
75
+
74
76
  when :Delimiter
75
77
  case tok
76
78
  when '('
77
79
  with_accepted(tok, typ) do
78
80
  grow_at_cursor(IdentityFuncTree.new) unless @cursor.is_a? FuncTree
79
- openarg = ArgTree.new('(')
80
- @stack << openarg
81
- grow_at_cursor(openarg)
81
+ unless @error
82
+ openarg = ArgTree.new('(')
83
+ @stack << openarg
84
+ grow_at_cursor(openarg)
85
+ end
82
86
  end
87
+
83
88
  when ')'
84
89
  break invalid_closing_delimiter_error(tok, typ) unless (@cursor = @stack.pop)
85
90
 
@@ -123,6 +128,7 @@ module OData
123
128
  end
124
129
  insert_before_cursor(binoptr)
125
130
  end
131
+
126
132
  when :BinopArithm
127
133
  with_accepted(tok, typ) do
128
134
  binoptr = BinopArithm.new(tok)
@@ -162,19 +168,21 @@ module OData
162
168
  @cursor.update_state(tok, typ)
163
169
  grow_at_cursor(FPNumber.new(tok))
164
170
  end
171
+
165
172
  when :unmatchedQuote
166
173
  break unmatched_quote_error(tok, typ)
174
+
175
+ when :space
176
+ with_accepted(tok, typ) do
177
+ @cursor.update_state(tok, typ)
178
+ end
167
179
  else
168
- raise 'Severe Error'
180
+ server_error
169
181
  end
170
- break if @error
171
- end
172
- begin
173
- @tree.check_types unless @error
174
- rescue ErrorInvalidArgumentType => e
175
- @error = e
182
+ break(@error) if @error
176
183
  end
177
- @error || @tree
184
+ (@error = @tree.check_types) unless @error
185
+ @error ? @error : Contract.valid(@tree)
178
186
  end
179
187
  end
180
188
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './base.rb'
4
- require_relative './sequel_function_adapter.rb'
3
+ require_relative './base'
4
+ require_relative './sequel_function_adapter'
5
5
 
6
- module OData
6
+ module Safrano
7
7
  module Filter
8
8
  # Base class for Leaves, Trees, RootTrees etc
9
9
  # class Node
@@ -16,8 +16,9 @@ module OData
16
16
  # RootTrees have childrens but no parent
17
17
  class RootTree
18
18
  def apply_to_dataset(dtcx, jh)
19
- filtexpr = @children.first.leuqes(jh)
20
- jh.dataset(dtcx).where(filtexpr).select_all(jh.start_model.table_name)
19
+ @children.first.leuqes(jh).if_valid do |filtexpr|
20
+ jh.dataset(dtcx).where(filtexpr).select_all(jh.start_model.table_name)
21
+ end
21
22
  end
22
23
 
23
24
  def sequel_expr(jh)
@@ -36,62 +37,102 @@ module OData
36
37
  def leuqes(jh)
37
38
  case @value
38
39
  when :startswith
39
- Sequel.like(args[0].leuqes(jh),
40
- args[1].leuqes_starts_like(jh))
40
+ Contract.collect_result!(args[0].leuqes(jh),
41
+ args[1].leuqes_starts_like(jh)) do |l0, l1|
42
+ Sequel.like(l0, l1)
43
+ end
44
+
41
45
  when :endswith
42
- Sequel.like(args[0].leuqes(jh),
43
- args[1].leuqes_ends_like(jh))
46
+ Contract.collect_result!(args[0].leuqes(jh),
47
+ args[1].leuqes_ends_like(jh)) do |l0, l1|
48
+ Sequel.like(l0, l1)
49
+ end
50
+
44
51
  when :substringof
45
52
 
46
53
  # there are multiple possible argument types (but all should return edm.string)
47
54
  if args[0].is_a?(QString)
48
55
  # substringof('Rhum', name) -->
49
56
  # name contains substr 'Rhum'
50
- Sequel.like(args[1].leuqes(jh),
51
- args[0].leuqes_substringof_sig1(jh))
57
+ Contract.collect_result!(args[1].leuqes(jh),
58
+ args[0].leuqes_substringof_sig1(jh)) do |l1, l0|
59
+ Sequel.like(l1, l0)
60
+ end
61
+
52
62
  # special non standard (ui5 client) case ?
53
63
  elsif args[0].is_a?(Literal) && args[1].is_a?(Literal)
54
- Sequel.like(args[1].leuqes(jh),
55
- args[0].leuqes_substringof_sig1(jh))
64
+ Contract.collect_result!(args[1].leuqes(jh),
65
+ args[0].leuqes_substringof_sig1(jh)) do |l1, l0|
66
+ Sequel.like(l1, l0)
67
+ end
68
+
56
69
  elsif args[1].is_a?(QString)
57
70
  substringof_sig2(jh) # adapter specific
58
71
  else
59
72
  # TODO... actually not supported?
60
- raise OData::Filter::Parser::ErrorFunctionArgumentType
73
+ raise Safrano::Filter::Parser::ErrorFunctionArgumentType
61
74
  end
62
75
  when :concat
63
- Sequel.join([args[0].leuqes(jh),
64
- args[1].leuqes(jh)])
76
+ Contract.collect_result!(args[0].leuqes(jh),
77
+ args[1].leuqes(jh)) do |l0, l1|
78
+ Sequel.join([l0, l1])
79
+ end
80
+
65
81
  when :length
66
- Sequel.char_length(args.first.leuqes(jh))
82
+ args.first.leuqes(jh)
83
+ .map_result! { |l| Sequel.char_length(l) }
84
+
67
85
  when :trim
68
- Sequel.trim(args.first.leuqes(jh))
86
+ args.first.leuqes(jh)
87
+ .map_result! { |l| Sequel.trim(l) }
88
+
69
89
  when :toupper
70
- Sequel.function(:upper, args.first.leuqes(jh))
90
+ args.first.leuqes(jh)
91
+ .map_result! { |l| Sequel.function(:upper, l) }
92
+
71
93
  when :tolower
72
- Sequel.function(:lower, args.first.leuqes(jh))
94
+ args.first.leuqes(jh)
95
+ .map_result! { |l| Sequel.function(:lower, l) }
96
+
73
97
  # all datetime funcs are adapter specific (because sqlite does not have extract)
74
98
  when :year
75
- year(jh)
99
+ args.first.leuqes(jh)
100
+ .map_result! { |l| year(l) }
101
+
76
102
  when :month
77
- month(jh)
103
+ args.first.leuqes(jh)
104
+ .map_result! { |l| month(l) }
105
+
78
106
  when :second
79
- second(jh)
107
+ args.first.leuqes(jh)
108
+ .map_result! { |l| second(l) }
109
+
80
110
  when :minute
81
- minute(jh)
111
+ args.first.leuqes(jh)
112
+ .map_result! { |l| minute(l) }
113
+
82
114
  when :hour
83
- hour(jh)
115
+ args.first.leuqes(jh)
116
+ .map_result! { |l| hour(l) }
117
+
84
118
  when :day
85
- day(jh)
119
+ args.first.leuqes(jh)
120
+ .map_result! { |l| day(l) }
121
+
86
122
  # math functions
87
123
  when :round
88
- Sequel.function(:round, args.first.leuqes(jh))
124
+ args.first.leuqes(jh)
125
+ .map_result! { |l| Sequel.function(:round, l) }
126
+
89
127
  when :floor
90
- floor(jh)
128
+ args.first.leuqes(jh)
129
+ .if_valid { |l| floor(l) }
130
+
91
131
  when :ceiling
92
- ceiling(jh)
132
+ args.first.leuqes(jh)
133
+ .if_valid { |l| ceiling(l) }
93
134
  else
94
- raise OData::FilterParseError
135
+ Safrano::FilterParseError
95
136
  end
96
137
  end
97
138
  end
@@ -110,9 +151,9 @@ module OData
110
151
  def leuqes(jh)
111
152
  case @value
112
153
  when :not
113
- Sequel.~(@children.first.leuqes(jh))
154
+ @children.first.leuqes(jh).map_result! { |l| Sequel.~(l) }
114
155
  else
115
- raise OData::FilterParseError
156
+ Safrano::FilterParseError
116
157
  end
117
158
  end
118
159
  end
@@ -138,12 +179,12 @@ module OData
138
179
  when :and
139
180
  :AND
140
181
  else
141
- raise OData::FilterParseError
182
+ return Safrano::FilterParseError
142
183
  end
143
-
144
- Sequel::SQL::BooleanExpression.new(leuqes_op,
145
- @children[0].leuqes(jh),
146
- @children[1].leuqes(jh))
184
+ Contract.collect_result!(@children[0].leuqes(jh),
185
+ @children[1].leuqes(jh)) do |c0, c1|
186
+ Sequel::SQL::BooleanExpression.new(leuqes_op, c0, c1)
187
+ end
147
188
  end
148
189
  end
149
190
 
@@ -162,12 +203,12 @@ module OData
162
203
  when :mod
163
204
  :%
164
205
  else
165
- raise OData::FilterParseError
206
+ return Safrano::FilterParseError
166
207
  end
167
-
168
- Sequel::SQL::NumericExpression.new(leuqes_op,
169
- @children[0].leuqes(jh),
170
- @children[1].leuqes(jh))
208
+ Contract.collect_result!(@children[0].leuqes(jh),
209
+ @children[1].leuqes(jh)) do |c0, c1|
210
+ Sequel::SQL::NumericExpression.new(leuqes_op, c0, c1)
211
+ end
171
212
  end
172
213
  end
173
214
 
@@ -178,42 +219,42 @@ module OData
178
219
  # Numbers (floating point, ints, dec)
179
220
  class FPNumber
180
221
  def leuqes(_jh)
181
- Sequel.lit(@value)
222
+ success Sequel.lit(@value)
182
223
  end
183
224
 
184
225
  def leuqes_starts_like(_jh)
185
- "#{@value}%"
226
+ success "#{@value}%"
186
227
  end
187
228
 
188
229
  def leuqes_ends_like(_jh)
189
- "%#{@value}"
230
+ success "%#{@value}"
190
231
  end
191
232
 
192
233
  def leuqes_substringof_sig1(_jh)
193
- "%#{@value}%"
234
+ success "%#{@value}%"
194
235
  end
195
236
  end
196
237
 
197
238
  # Literals are unquoted words
198
239
  class Literal
199
240
  def leuqes(jh)
200
- raise OData::Filter::Parser::ErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
241
+ return Safrano::FilterParseErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
201
242
 
202
- Sequel[jh.start_model.table_name][@value.to_sym]
243
+ success Sequel[jh.start_model.table_name][@value.to_sym]
203
244
  end
204
245
 
205
246
  # non stantard extensions to support things like
206
247
  # substringof(Rhum, name) ????
207
248
  def leuqes_starts_like(_jh)
208
- "#{@value}%"
249
+ success "#{@value}%"
209
250
  end
210
251
 
211
252
  def leuqes_ends_like(_jh)
212
- "%#{@value}"
253
+ success "%#{@value}"
213
254
  end
214
255
 
215
256
  def leuqes_substringof_sig1(_jh)
216
- "%#{@value}%"
257
+ success "%#{@value}%"
217
258
  end
218
259
 
219
260
  def as_string
@@ -226,26 +267,26 @@ module OData
226
267
  def leuqes(jh)
227
268
  jh.add(@path)
228
269
  talias = jh.start_model.get_alias_sym(@path)
229
- Sequel[talias][@attrib.to_sym]
270
+ success Sequel[talias][@attrib.to_sym]
230
271
  end
231
272
  end
232
273
 
233
274
  # Quoted Strings
234
275
  class QString
235
276
  def leuqes(_jh)
236
- @value
277
+ success @value
237
278
  end
238
279
 
239
280
  def leuqes_starts_like(_jh)
240
- "#{@value}%"
281
+ success "#{@value}%"
241
282
  end
242
283
 
243
284
  def leuqes_ends_like(_jh)
244
- "%#{@value}"
285
+ success "%#{@value}"
245
286
  end
246
287
 
247
288
  def leuqes_substringof_sig1(_jh)
248
- "%#{@value}%"
289
+ success "%#{@value}%"
249
290
  end
250
291
  end
251
292
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './tree.rb'
4
- require_relative './sequel.rb'
3
+ require_relative './tree'
4
+ require_relative './sequel'
5
5
 
6
- module OData
6
+ module Safrano
7
7
  module Filter
8
8
  # sqlite adapter specific function handler
9
9
  module FuncTreeSqlite
@@ -11,10 +11,11 @@ module OData
11
11
  # substringof(name, '__Route du Rhum__') -->
12
12
  # '__Route du Rhum__' contains name as a substring
13
13
  # sqlite uses instr()
14
-
15
- substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
16
-
17
- Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
14
+ Contract.collect_result!(args[1].leuqes(jh),
15
+ args[0].leuqes(jh)) do |l1, l0|
16
+ substr_func = Sequel.function(:instr, l1, l0)
17
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
18
+ end
18
19
  end
19
20
  # %d day of month: 00
20
21
  # %f fractional seconds: SS.SSS
@@ -31,77 +32,73 @@ module OData
31
32
  # %% %
32
33
 
33
34
  # sqlite does not have extract but recommends to use strftime
34
- def year(jh)
35
- Sequel.function(:strftime, '%Y', args.first.leuqes(jh)).cast(:integer)
35
+ def year(lq)
36
+ Sequel.function(:strftime, '%Y', lq).cast(:integer)
36
37
  end
37
38
 
38
- def month(jh)
39
- Sequel.function(:strftime, '%m', args.first.leuqes(jh)).cast(:integer)
39
+ def month(lq)
40
+ Sequel.function(:strftime, '%m', lq).cast(:integer)
40
41
  end
41
42
 
42
- def second(jh)
43
- Sequel.function(:strftime, '%S', args.first.leuqes(jh)).cast(:integer)
43
+ def second(lq)
44
+ Sequel.function(:strftime, '%S', lq).cast(:integer)
44
45
  end
45
46
 
46
- def minute(jh)
47
- Sequel.function(:strftime, '%M', args.first.leuqes(jh)).cast(:integer)
47
+ def minute(lq)
48
+ Sequel.function(:strftime, '%M', lq).cast(:integer)
48
49
  end
49
50
 
50
- def hour(jh)
51
- Sequel.function(:strftime, '%H', args.first.leuqes(jh)).cast(:integer)
51
+ def hour(lq)
52
+ Sequel.function(:strftime, '%H', lq).cast(:integer)
52
53
  end
53
54
 
54
- def day(jh)
55
- Sequel.function(:strftime, '%d', args.first.leuqes(jh)).cast(:integer)
55
+ def day(lq)
56
+ Sequel.function(:strftime, '%d', lq).cast(:integer)
56
57
  end
57
58
 
58
- def floor(jh)
59
- raise OData::Filter::FunctionNotImplemented, "$filter function 'floor' is not implemented in sqlite adapter"
59
+ def floor(_lq)
60
+ Safrano::FilterFunctionNotImplementedError.new("$filter function 'floor' is not implemented in sqlite adapter")
60
61
  end
61
62
 
62
- def ceiling(jh)
63
- raise OData::Filter::FunctionNotImplemented, "$filter function 'ceiling' is not implemented in sqlite adapter"
63
+ def ceiling(_lq)
64
+ Safrano::FilterFunctionNotImplementedError.new("$filter function 'ceiling' is not implemented in sqlite adapter")
64
65
  end
65
66
  end
66
67
  # re-useable module with math floor/ceil functions for those adapters having these SQL funcs
67
68
  module MathFloorCeilFuncTree
68
- def floor(jh)
69
- Sequel.function(:floor, args.first.leuqes(jh))
69
+ def floor(lq)
70
+ success Sequel.function(:floor, lq)
70
71
  end
71
72
 
72
- def ceiling(jh)
73
- Sequel.function(:ceil, args.first.leuqes(jh))
73
+ def ceiling(lq)
74
+ success Sequel.function(:ceil, lq)
74
75
  end
75
76
  end
76
77
 
77
78
  # re-useable module with Datetime functions with extract()
78
79
  module DateTimeFuncTreeExtract
79
- def year(jh)
80
- args.first.leuqes(jh).extract(:year)
81
- end
82
-
83
- def year(jh)
84
- args.first.leuqes(jh).extract(:year)
80
+ def year(lq)
81
+ lq.extract(:year)
85
82
  end
86
83
 
87
- def month(jh)
88
- args.first.leuqes(jh).extract(:month)
84
+ def month(lq)
85
+ lq.extract(:month)
89
86
  end
90
87
 
91
- def second(jh)
92
- args.first.leuqes(jh).extract(:second)
88
+ def second(lq)
89
+ lq.extract(:second)
93
90
  end
94
91
 
95
- def minute(jh)
96
- args.first.leuqes(jh).extract(:minute)
92
+ def minute(lq)
93
+ lq.extract(:minute)
97
94
  end
98
95
 
99
- def hour(jh)
100
- args.first.leuqes(jh).extract(:hour)
96
+ def hour(lq)
97
+ lq.extract(:hour)
101
98
  end
102
99
 
103
- def day(jh)
104
- args.first.leuqes(jh).extract(:day)
100
+ def day(lq)
101
+ lq.extract(:day)
105
102
  end
106
103
  end
107
104
 
@@ -111,9 +108,11 @@ module OData
111
108
  # substringof(name, '__Route du Rhum__') -->
112
109
  # '__Route du Rhum__' contains name as a substring
113
110
  # postgres does not know instr() but has strpos
114
- substr_func = Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
115
-
116
- Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
111
+ Contract.collect_result!(args[1].leuqes(jh),
112
+ args[0].leuqes(jh)) do |l1, l0|
113
+ substr_func = Sequel.function(:strpos, l1, l0)
114
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
115
+ end
117
116
  end
118
117
 
119
118
  # postgres uses extract()
@@ -132,9 +131,11 @@ module OData
132
131
  # substringof(name, '__Route du Rhum__') -->
133
132
  # '__Route du Rhum__' contains name as a substring
134
133
  # instr() seems to be the most common substring func
135
- substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
136
-
137
- Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
134
+ Contract.collect_result!(args[1].leuqes(jh),
135
+ args[0].leuqes(jh)) do |l1, l0|
136
+ substr_func = Sequel.function(:instr, l1, l0)
137
+ Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
138
+ end
138
139
  end
139
140
 
140
141
  # XYZ uses extract() ?