safrano 0.3.2 → 0.3.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.
data/lib/odata/error.rb CHANGED
@@ -14,6 +14,13 @@ module OData
14
14
  super("class #{name} is not a Sequel Model", name)
15
15
  end
16
16
  end
17
+ # when published class as media does not have the mandatory media fields
18
+ class MediaModelError < NameError
19
+ def initialize(name)
20
+ super("Model #{name} does not have the mandatory media attributes content_type/media_src", name)
21
+ end
22
+ end
23
+
17
24
  # when published association was not defined on Sequel level
18
25
  class ModelAssociationNameError < NameError
19
26
  def initialize(klass, symb)
@@ -27,13 +34,11 @@ module OData
27
34
  # base module for HTTP errors
28
35
  module Error
29
36
  def odata_get(req)
30
- if req.accept?('application/json')
31
- [const_get(:HTTP_CODE),
32
- { 'Content-Type' => 'application/json;charset=utf-8' },
37
+ if req.accept?(APPJSON)
38
+ [const_get(:HTTP_CODE), CT_JSON,
33
39
  { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
34
40
  else
35
- [const_get(:HTTP_CODE),
36
- { 'Content-Type' => 'text/plain;charset=utf-8' }, @msg]
41
+ [const_get(:HTTP_CODE), CT_TEXT, @msg]
37
42
  end
38
43
  end
39
44
  end
@@ -49,6 +54,12 @@ module OData
49
54
  @msg = 'Bad Request: Failed changeset '
50
55
  end
51
56
 
57
+ # $value request for a non-media entity
58
+ class BadRequestNonMediaValue < BadRequestError
59
+ HTTP_CODE = 400
60
+ @msg = 'Bad Request: $value request for a non-media entity'
61
+ end
62
+
52
63
  # for Syntax error in Filtering
53
64
  class BadRequestFilterParseError < BadRequestError
54
65
  HTTP_CODE = 400
@@ -0,0 +1,119 @@
1
+ require 'json'
2
+ require_relative '../safrano_core.rb'
3
+ require_relative './entity.rb'
4
+
5
+ module OData
6
+ # link newly created entities(child) to an existing parent
7
+ # by following the association_reflection rules
8
+ def OData.create_nav_relation(child, assoc, parent)
9
+ return unless assoc
10
+
11
+ # Note: this coding shares some bits from our sequel/plugins/join_by_paths,
12
+ # method build_unique_join_segments
13
+ # eventually there is an opportunity to have more reusable code here
14
+ case assoc[:type]
15
+ when :one_to_many, :one_to_one
16
+ # sets the FK values in child to corresponding related parent key-values
17
+ # thus creating the "link" between the new entity and the parent
18
+ # if a FK value is already set (not nil/NULL) then only check the
19
+ # consistency with the corresponding parent key-value
20
+ # If the FK value and the parent key value are different, then it's a
21
+ # a Bad Request error
22
+
23
+ leftm = assoc[:model] # should be same as parent.class
24
+ lks = [leftm.primary_key].flatten
25
+ rks = [assoc[:key]].flatten
26
+ join_cond = rks.zip(lks).to_h
27
+ join_cond.each { |rk, lk|
28
+ if child.values[rk] # FK in new entity from payload not nil, only check consistency
29
+ # with the parent - id(s)
30
+ if (child.values[rk] != parent.pk_hash[lk]) # error...
31
+ # TODO
32
+ end
33
+ else # we can set the FK value, thus creating the "link"
34
+ child.set(rk => parent.pk_hash[lk])
35
+ end
36
+ }
37
+ when :many_to_one
38
+ # sets the FK values in parent to corresponding related child key-values
39
+ # thus creating the "link" between the new entity and the parent
40
+ # Per design, this can only be called when the FK value is nil
41
+ # from NilNavigationAttribute.odata_post
42
+ lks = [assoc[:key]].flatten
43
+ rks = [child.class.primary_key].flatten
44
+ join_cond = rks.zip(lks).to_h
45
+ join_cond.each { |rk, lk|
46
+ if parent.values[lk] # FK in parent not nil, only check consistency
47
+ # with the child - id(s)
48
+ if (parent.values[lk] != child.pk_hash[rk]) # error...
49
+ # TODO
50
+ end
51
+ else # we can set the FK value, thus creating the "link"
52
+ parent.set(lk => child.pk_hash[rk])
53
+ end
54
+ }
55
+ end
56
+ end
57
+
58
+ # Represents a named but nil-valued navigation-attribute of an Entity
59
+ # (usually resulting from a NULL FK db value)
60
+ class NilNavigationAttribute
61
+ attr_reader :name
62
+ attr_reader :parent
63
+ def initialize(parent, name)
64
+ @parent = parent
65
+ @name = name
66
+ @navattr_reflection = parent.class.association_reflections[name.to_sym]
67
+ @klass = @navattr_reflection[:class_name].constantize
68
+ end
69
+
70
+ def odata_get(req)
71
+ if req.walker.media_value
72
+ OData::ErrorNotFound.odata_get
73
+ elsif req.accept?(APPJSON)
74
+ [200, CT_JSON, to_odata_json(service: req.service)]
75
+ else # TODO: other formats
76
+ 415
77
+ end
78
+ end
79
+
80
+ # create the nav. entity
81
+ def odata_post(req)
82
+ # delegate to the class method
83
+ @klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
84
+ end
85
+
86
+ # create the nav. entity
87
+ def odata_put(req)
88
+ # delegate to the class method
89
+ @klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
90
+ end
91
+
92
+ # empty output as OData json (v2)
93
+ def to_odata_json(*)
94
+ { 'd' => {} }.to_json
95
+ end
96
+
97
+ # for testing purpose (assert_equal ...)
98
+ def ==(other)
99
+ (@parent == other.parent) && (@name == other.name)
100
+ end
101
+
102
+ # methods related to transitions to next state (cf. walker)
103
+ module Transitions
104
+ def transition_end(_match_result)
105
+ [nil, :end]
106
+ end
107
+
108
+ def transition_value(_match_result)
109
+ [self, :end_with_value]
110
+ end
111
+
112
+ def allowed_transitions
113
+ [Safrano::TransitionEnd,
114
+ Safrano::TransitionValue]
115
+ end
116
+ end
117
+ include Transitions
118
+ end
119
+ end
data/lib/odata/walker.rb CHANGED
@@ -21,9 +21,12 @@ module OData
21
21
  # is $count requested?
22
22
  attr_accessor :do_count
23
23
 
24
- # is $value requested?
24
+ # is $value (of attribute) requested?
25
25
  attr_reader :raw_value
26
26
 
27
+ # is $value (of media entity) requested?
28
+ attr_reader :media_value
29
+
27
30
  # are $links requested ?
28
31
  attr_reader :do_links
29
32
 
@@ -35,9 +38,11 @@ module OData
35
38
  @content_id_refs = content_id_refs
36
39
 
37
40
  @contexts = [@context]
41
+
38
42
  @path_start = @path_remain = if service
39
43
  unprefixed(service.xpath_prefix, path)
40
44
  else # This is for batch function
45
+
41
46
  path
42
47
  end
43
48
  @path_done = ''
@@ -51,7 +56,9 @@ module OData
51
56
  if (prefix == '') || (prefix == '/')
52
57
  path
53
58
  else
54
- path.sub!(/\A#{prefix}/, '')
59
+ # path.sub!(/\A#{prefix}/, '')
60
+ # TODO check
61
+ path.sub(/\A#{prefix}/, '')
55
62
  end
56
63
  end
57
64
 
@@ -107,6 +114,9 @@ module OData
107
114
  when :end_with_value
108
115
  @raw_value = true
109
116
  @status = :end
117
+ when :end_with_media_value
118
+ @media_value = true
119
+ @status = :end
110
120
  when :run_with_links
111
121
  @do_links = true
112
122
  @status = :run
@@ -7,7 +7,7 @@ module Rack
7
7
  # used
8
8
  class Builder < ::Rack::Builder
9
9
  def initialize(default_app = nil, &block)
10
- super(default_app)
10
+ super(default_app) {}
11
11
  use ::Rack::Cors
12
12
  instance_eval(&block) if block_given?
13
13
  use ::Rack::Lint
data/lib/rack_app.rb CHANGED
@@ -26,12 +26,7 @@ module OData
26
26
  return @walker.error.odata_get(@request) unless @walker.error.nil?
27
27
 
28
28
  # this is too critical; raise a real Exception
29
- # begin
30
29
  raise 'Walker construction failed with a unknown Error '
31
- # rescue StandardError
32
- # binding.pry
33
- # end
34
- # [500, {}, 'Server Error']
35
30
  end
36
31
 
37
32
  def odata_delete
@@ -42,6 +37,14 @@ module OData
42
37
  end
43
38
  end
44
39
 
40
+ def odata_put
41
+ if @walker.status == :end
42
+ @walker.end_context.odata_put(@request)
43
+ else
44
+ odata_error
45
+ end
46
+ end
47
+
45
48
  def odata_patch
46
49
  if @walker.status == :end
47
50
  @walker.end_context.odata_patch(@request)
@@ -67,7 +70,7 @@ module OData
67
70
  end
68
71
 
69
72
  def odata_head
70
- [200, {}, '']
73
+ [200, {}, ['']]
71
74
  end
72
75
  end
73
76
 
@@ -105,7 +108,9 @@ module OData
105
108
  odata_delete
106
109
  when 'OPTIONS'
107
110
  odata_options
108
- when 'PATCH', 'PUT', 'MERGE'
111
+ when 'PUT'
112
+ odata_put
113
+ when 'PATCH', 'MERGE'
109
114
  odata_patch
110
115
  else
111
116
  raise Error
data/lib/request.rb CHANGED
@@ -10,6 +10,7 @@ module OData
10
10
  HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
11
11
  HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'.freeze
12
12
  HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
13
+ ON_CGST_ERROR = (proc { |r| raise(Sequel::Rollback) if r.in_changeset })
13
14
 
14
15
  # borowed from Sinatra
15
16
  class AcceptEntry
@@ -94,7 +95,7 @@ module OData
94
95
  end
95
96
 
96
97
  def create_odata_walker
97
- @walker = Walker.new(@service, path_info, @content_id_references)
98
+ @env['safrano.walker'] = @walker = Walker.new(@service, path_info, @content_id_references)
98
99
  end
99
100
 
100
101
  def accept
@@ -132,13 +133,24 @@ module OData
132
133
  end
133
134
  end
134
135
 
135
- def with_parsed_data(on_error: nil)
136
+ def with_media_data
137
+ if (filename = @env['HTTP_SLUG'])
138
+
139
+ yield @env['rack.input'], content_type.split(';').first, filename
140
+
141
+ else
142
+ ON_CGST_ERROR.call(self)
143
+ return [400, {}, ['File upload error: Missing SLUG']]
144
+ end
145
+ end
146
+
147
+ def with_parsed_data
136
148
  if content_type == APPJSON
137
149
  # Parse json payload
138
150
  begin
139
151
  data = JSON.parse(body.read)
140
152
  rescue JSON::ParserError => e
141
- on_error.call if on_error
153
+ ON_CGST_ERROR.call(self)
142
154
  return [400, {}, ['JSON Parser Error while parsing payload : ',
143
155
  e.message]]
144
156
  end
data/lib/safrano.rb CHANGED
@@ -6,6 +6,7 @@ require_relative './multipart.rb'
6
6
  require 'safrano_core.rb'
7
7
  require 'odata/entity.rb'
8
8
  require 'odata/attribute.rb'
9
+ require 'odata/navigation_attribute.rb'
9
10
  require 'odata/collection.rb'
10
11
  require 'service.rb'
11
12
  require 'odata/walker.rb'
data/lib/safrano_core.rb CHANGED
@@ -5,10 +5,11 @@ module OData
5
5
  # some prominent constants... probably already defined elsewhere eg in Rack
6
6
  # but lets KISS
7
7
  CONTENT_TYPE = 'Content-Type'.freeze
8
+ CTT_TYPE_LC = 'content-type'.freeze
8
9
  TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'.freeze
9
10
  APPJSON = 'application/json'.freeze
10
11
  APPXML = 'application/xml'.freeze
11
-
12
+ MP_MIXED = 'multipart/mixed'.freeze
12
13
  APPXML_UTF8 = 'application/xml;charset=utf-8'.freeze
13
14
  APPATOMXML_UTF8 = 'application/atomsvc+xml;charset=utf-8'.freeze
14
15
  APPJSON_UTF8 = 'application/json;charset=utf-8'.freeze
data/lib/service.rb CHANGED
@@ -103,12 +103,9 @@ module OData
103
103
  end
104
104
 
105
105
  def get_entity_odata_h(entity:, expand: nil, uribase:)
106
- hres = {}
106
+ hres = entity.casted_values
107
107
  hres['__metadata'] = entity.metadata_h(uribase: uribase)
108
108
 
109
- # hres.merge!(entity.values)
110
- hres.merge!(entity.casted_values)
111
-
112
109
  nav_values_h = {}
113
110
  nav_coll_h = {}
114
111
 
@@ -203,6 +200,7 @@ module OData
203
200
 
204
201
  DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
205
202
  TRAILING_SLASH = %r{/\z}.freeze
203
+ DEFAULT_PATH_PREFIX = '/'
206
204
 
207
205
  # input is the DataServiceVersion request header string, eg.
208
206
  # '2.0;blabla' ---> Version -> 2
@@ -260,18 +258,38 @@ module OData
260
258
  other
261
259
  end
262
260
 
263
- def register_model(modelklass, entity_set_name = nil)
261
+ def register_model(modelklass, entity_set_name = nil, is_media = false)
264
262
  # check that the provided klass is a Sequel Model
265
263
  unless modelklass.is_a? Sequel::Model::ClassMethods
266
264
  raise OData::API::ModelNameError, modelklass
267
265
  end
268
266
 
269
- if modelklass.primary_key.is_a?(Array)
270
- modelklass.extend OData::EntityClassMultiPK
271
- modelklass.include OData::EntityMultiPK
267
+ if modelklass.ancestors.include? OData::Entity
268
+ # modules were already added previously;
269
+ # cleanup state to avoid having data from previous calls
270
+ # mostly usefull for testing (eg API)
271
+ modelklass.reset
272
+ else # first API call... (normal non-testing case)
273
+ if modelklass.primary_key.is_a?(Array)
274
+ modelklass.extend OData::EntityClassMultiPK
275
+ modelklass.include OData::EntityMultiPK
276
+ else
277
+ modelklass.extend OData::EntityClassSinglePK
278
+ modelklass.include OData::EntitySinglePK
279
+ end
280
+ end
281
+ # Media/Non-media
282
+ if is_media
283
+ modelklass.extend OData::EntityClassMedia
284
+ # set default media handler . Can be overridden later with the
285
+ # "use HandlerKlass, options" API
286
+
287
+ modelklass.set_default_media_handler
288
+ modelklass.api_check_media_fields
289
+ modelklass.include OData::MediaEntity
272
290
  else
273
- modelklass.extend OData::EntityClassSinglePK
274
- modelklass.include OData::EntitySinglePK
291
+ modelklass.extend OData::EntityClassNonMedia
292
+ modelklass.include OData::NonMediaEntity
275
293
  end
276
294
 
277
295
  modelklass.prepare_pk
@@ -290,6 +308,14 @@ module OData
290
308
  modelklass.deferred_iblock = block if block_given?
291
309
  end
292
310
 
311
+ def publish_media_model(modelklass, entity_set_name = nil, &block)
312
+ register_model(modelklass, entity_set_name, true)
313
+ # we need to execute the passed block in a deferred step
314
+ # after all models have been registered (due to rel. dependancies)
315
+ # modelklass.instance_eval(&block) if block_given?
316
+ modelklass.deferred_iblock = block if block_given?
317
+ end
318
+
293
319
  def cmap=(imap)
294
320
  @cmap = imap
295
321
  set_collections_sorted(@cmap.values)
@@ -319,6 +345,9 @@ module OData
319
345
  # now that we know all model klasses we can handle relationships
320
346
  execute_deferred_iblocks
321
347
 
348
+ # set default path prefix if path_prefix was not called
349
+ path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
350
+
322
351
  # and finally build the path list
323
352
  @collections.each(&:build_attribute_path_list)
324
353
  end
@@ -497,7 +526,7 @@ module OData
497
526
  # Documents MUST be
498
527
  # identified with the "application/atomsvc+xml" media type (see
499
528
  # [RFC5023] section 8).
500
- [200, CT_ATOMXML, service_xml(req)]
529
+ [200, CT_ATOMXML, [service_xml(req)]]
501
530
  else
502
531
  # this is returned by http://services.odata.org/V2/OData/OData.svc
503
532
  415
@@ -570,7 +599,7 @@ module OData
570
599
 
571
600
  def odata_get(req)
572
601
  if req.accept?(APPXML)
573
- [200, CT_APPXML, @service.metadata_xml(req)]
602
+ [200, CT_APPXML, [@service.metadata_xml(req)]]
574
603
  else
575
604
  415
576
605
  end
data/lib/version.rb ADDED
@@ -0,0 +1,4 @@
1
+
2
+ module Safrano
3
+ VERSION = '0.3.3'
4
+ end