safrano 0.3.2 → 0.3.3

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