safrano 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 49dc75cf9bfcdff0a03b38eaada7f0a968a09ab3777327f0c893f8f39fd32d8f
4
+ data.tar.gz: 7103e882037de0e6c89d368ec25cb0f6c6b9ed4be7fc8dff9aa64d0a6107a29d
5
+ SHA512:
6
+ metadata.gz: e7dbd2370716c6e5f323c9fff63394053521f70af27ee613a288e7ca089071818844a09dc4e400cba4fcd0e227e4c7d38b5d07d2e887aa33553cf8ea57a1e5ec
7
+ data.tar.gz: 94b84b19d4d7273a0aef7c4a52348e4e77340f95cf64792e6f47dc1cde8e06ae675ee3bc5cd87d8c58e8e8098b914b24bb4e38818f96f75fd2116f32ae6e0d8b
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rack'
4
+ require 'rack/cors'
5
+
6
+ module Rack
7
+ module OData
8
+ # just a Wrapper to ensure (force?) that mandatory middlewares are acutally
9
+ # used
10
+ class Builder < ::Rack::Builder
11
+ def initialize(default_app = nil, &block)
12
+ super(default_app)
13
+ use ::Rack::Cors
14
+ instance_eval(&block) if block_given?
15
+ use ::Rack::Lint
16
+ use ::Rack::ContentLength
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/rack_app.rb ADDED
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rack'
4
+ require_relative 'odata/walker.rb'
5
+ require_relative 'request.rb'
6
+ require_relative 'response.rb'
7
+ require 'pry'
8
+
9
+ module OData
10
+ # handle GET PUT etc
11
+ module MethodHandlers
12
+ def odata_options
13
+ # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
14
+
15
+ x = if @walker.status == :end
16
+ headers.delete('Content-Type')
17
+ @response.headers.delete('Content-Type')
18
+ [200, {}, '']
19
+ else
20
+ odata_error
21
+ end
22
+ @response.headers['Content-Type'] = ''
23
+ x
24
+ end
25
+
26
+ def odata_error
27
+ if @walker.error.nil?
28
+ [500, {}, 'Server Error']
29
+ else
30
+ @walker.error.odata_get(@request)
31
+ end
32
+ end
33
+
34
+ def odata_delete
35
+ if @walker.status == :end
36
+ @walker.end_context.odata_delete(@request)
37
+ else
38
+ odata_error
39
+ end
40
+ end
41
+
42
+ def odata_patch
43
+ if @walker.status == :end
44
+ @walker.end_context.odata_patch(@request)
45
+ else
46
+ odata_error
47
+ end
48
+ end
49
+
50
+ def odata_get
51
+ if @walker.status == :end
52
+ @walker.end_context.odata_get(@request)
53
+ else
54
+ odata_error
55
+ end
56
+ end
57
+
58
+ def odata_post
59
+ if @walker.status == :end
60
+ @walker.end_context.odata_post(@request)
61
+ else
62
+ odata_error
63
+ end
64
+ end
65
+
66
+ def odata_head
67
+ [200, {}, '']
68
+ end
69
+ end
70
+
71
+ # the main Rack server app. Source: the Rack docu/examples and partly
72
+ # inspired from Sinatra
73
+ class ServerApp
74
+ METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|PUT|DELETE')
75
+ include MethodHandlers
76
+ def before
77
+ headers 'Cache-Control' => 'no-cache'
78
+ headers 'Expires' => '-1'
79
+ headers 'Pragma' => 'no-cache'
80
+
81
+ @request.service_base = self.class.get_service_base
82
+
83
+ neg_error = @request.negotiate_service_version
84
+
85
+ raise Error if neg_error
86
+
87
+ return false unless @request.service
88
+
89
+ headers 'DataServiceVersion' => @request.service.data_service_version
90
+ end
91
+
92
+ # dispatch for all methods requiring parsing of the path
93
+ # with walker (ie. allmost all excepted HEAD)
94
+ def dispatch_with_walker
95
+ @walker = @request.create_odata_walker
96
+ case @request.request_method
97
+ when 'GET'
98
+ odata_get
99
+ when 'POST'
100
+ odata_post
101
+ when 'DELETE'
102
+ odata_delete
103
+ when 'OPTIONS'
104
+ odata_options
105
+ when 'PATCH', 'PUT'
106
+ odata_patch
107
+ else
108
+ raise Error
109
+ end
110
+ end
111
+
112
+ def dispatch
113
+ req_ret = if @request.request_method !~ METHODS_REGEXP
114
+ [404, {}, ['Did you get lost?']]
115
+ elsif @request.request_method == 'HEAD'
116
+ odata_head
117
+ else
118
+ dispatch_with_walker
119
+ end
120
+ @response.status, rsph, @response.body = req_ret
121
+ headers rsph
122
+ end
123
+
124
+ def call(env)
125
+ @request = OData::Request.new(env)
126
+ @response = OData::Response.new
127
+
128
+ before
129
+
130
+ dispatch
131
+
132
+ @response.finish
133
+ end
134
+
135
+ # Set multiple response headers with Hash.
136
+ def headers(hash = nil)
137
+ @response.headers.merge! hash if hash
138
+ @response.headers
139
+ end
140
+
141
+ def self.enable_batch
142
+ @service_base.enable_batch
143
+ end
144
+
145
+ def self.get_service_base
146
+ @service_base
147
+ end
148
+
149
+ def self.set_servicebase(sbase, p_prefix = '')
150
+ @service_base = sbase
151
+ @service_base.path_prefix p_prefix
152
+ @service_base.enable_v1_service
153
+ @service_base.enable_v2_service
154
+ end
155
+
156
+ def self.publish_service(&block)
157
+ sbase = OData::ServiceBase.new
158
+ sbase.instance_eval(&block) if block_given?
159
+ sbase.finalize_publishing
160
+ set_servicebase(sbase)
161
+ end
162
+ end
163
+ end
data/lib/request.rb ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rack'
4
+
5
+ module OData
6
+ # monkey patch deactivate Rack/multipart because it does not work on simple
7
+ # OData $batch requests when the content-length
8
+ # is not passed
9
+ class Request < Rack::Request
10
+ HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
11
+ # HEADER_VALUE_WITH_PARAMS = /(?:(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*)
12
+ # \s*(?:;#{HEADER_PARAM})*/
13
+ HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'.freeze
14
+ HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
15
+
16
+ # borowed from Sinatra
17
+ class AcceptEntry
18
+ attr_accessor :params
19
+ attr_reader :entry
20
+ def initialize(entry)
21
+ params = entry.scan(HEADER_PARAM).map! do |s|
22
+ key, value = s.strip.split('=', 2)
23
+ value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"')
24
+ [key, value]
25
+ end
26
+
27
+ @entry = entry
28
+ @type = entry[/[^;]+/].delete(' ')
29
+ @params = Hash[params]
30
+ @q = @params.delete('q') { 1.0 }.to_f
31
+ end
32
+
33
+ def method_missing(*args, &block)
34
+ to_str.send(*args, &block)
35
+ end
36
+
37
+ def <=>(other)
38
+ other.priority <=> priority
39
+ end
40
+
41
+ def priority
42
+ # We sort in descending order; better matches should be higher.
43
+ [@q, -@type.count('*'), @params.size]
44
+ end
45
+
46
+ def respond_to?(*args)
47
+ super || to_str.respond_to?(*args)
48
+ end
49
+
50
+ def to_s(full = false)
51
+ full ? entry : to_str
52
+ end
53
+
54
+ def to_str
55
+ @type
56
+ end
57
+ end
58
+ ## original coding:
59
+ # # The set of media-types. Requests that do not indicate
60
+ # # one of the media types presents in this list will not be eligible
61
+ # # for param parsing like soap attachments or generic multiparts
62
+ # PARSEABLE_DATA_MEDIA_TYPES = [
63
+ # 'multipart/related',
64
+ # 'multipart/mixed'
65
+ # ]
66
+ # # Determine whether the request body contains data by checking
67
+ # # the request media_type against registered parse-data media-types
68
+ # def parseable_data?
69
+ # PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
70
+ # end
71
+ def parseable_data?
72
+ false
73
+ end
74
+ # OData extension
75
+ attr_accessor :service_base
76
+ attr_accessor :service
77
+ attr_accessor :walker
78
+
79
+ def create_odata_walker
80
+ @walker = Walker.new(@service, path_info)
81
+ end
82
+
83
+ def accept
84
+ @env['safrano.accept'] ||= begin
85
+ if @env.include?('HTTP_ACCEPT') && (@env['HTTP_ACCEPT'].to_s != '')
86
+ @env['HTTP_ACCEPT'].to_s.scan(HEADER_VAL_WITH_PAR)
87
+ .map! { |e| AcceptEntry.new(e) }.sort
88
+ else
89
+ [AcceptEntry.new('*/*')]
90
+ end
91
+ end
92
+ end
93
+
94
+ def uribase
95
+ return @uribase if @uribase
96
+
97
+ @uribase =
98
+ "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{service.xpath_prefix}"
99
+ end
100
+
101
+ def accept?(type)
102
+ preferred_type(type).to_s.include?(type)
103
+ end
104
+
105
+ def preferred_type(*types)
106
+ accepts = accept # just evaluate once
107
+ return accepts.first if types.empty?
108
+
109
+ types.flatten!
110
+ return types.first if accepts.empty?
111
+
112
+ accepts.detect do |pattern|
113
+ type = types.detect { |t| File.fnmatch(pattern, t) }
114
+ return type if type
115
+ end
116
+ end
117
+
118
+ def negotiate_service_version
119
+ maxv = if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
120
+ OData::ServiceBase.parse_data_service_version(rqv)
121
+ else
122
+ OData::MAX_DATASERVICE_VERSION
123
+ end
124
+ return OData::BadRequestError if maxv.nil?
125
+ # client request an too old version --> 501
126
+ return OData::NotImplementedError if maxv < OData::MIN_DATASERVICE_VERSION
127
+
128
+ minv = if (rqv = env['HTTP_MINDATASERVICEVERSION'])
129
+ OData::ServiceBase.parse_data_service_version(rqv)
130
+ else
131
+ OData::MIN_DATASERVICE_VERSION
132
+ end
133
+ return OData::BadRequestError if minv.nil?
134
+ # client request an too new version --> 501
135
+ return OData::NotImplementedError if minv > OData::MAX_DATASERVICE_VERSION
136
+ return OData::BadRequestError if minv > maxv
137
+
138
+ v = if (rqv = env['HTTP_DATASERVICEVERSION'])
139
+ OData::ServiceBase.parse_data_service_version(rqv)
140
+ else
141
+ OData::MAX_DATASERVICE_VERSION
142
+ end
143
+
144
+ return OData::BadRequestError if v.nil?
145
+ return OData::NotImplementedError if v > OData::MAX_DATASERVICE_VERSION
146
+ return OData::NotImplementedError if v < OData::MIN_DATASERVICE_VERSION
147
+
148
+ @service = nil
149
+ @service = case maxv
150
+ when '1'
151
+ @service_base.v1
152
+ when '2', '3', '4'
153
+ @service_base.v2
154
+ end
155
+ nil
156
+ end
157
+ end
158
+ end
data/lib/response.rb ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rack'
3
+
4
+ # monkey patch deactivate Rack/multipart because it does not work on simple
5
+ # OData $batch requests when the content-length is not passed
6
+ module OData
7
+ # borrowed fro Sinatra
8
+ # The response object. See Rack::Response and Rack::Response::Helpers for
9
+ # more info:
10
+ # http://rubydoc.info/github/rack/rack/master/Rack/Response
11
+ # http://rubydoc.info/github/rack/rack/master/Rack/Response/Helpers
12
+ class Response < ::Rack::Response
13
+ DROP_BODY_RESPONSES = [204, 205, 304].freeze
14
+ def initialize(*)
15
+ super
16
+ headers['Content-Type'] ||= 'text/html'
17
+ end
18
+
19
+ def body=(value)
20
+ value = value.body while ::Rack::Response === value
21
+ @body = String === value ? [value.to_str] : value
22
+ end
23
+
24
+ def each
25
+ block_given? ? super : enum_for(:each)
26
+ end
27
+
28
+ def finish
29
+ result = body
30
+
31
+ if drop_content_info?
32
+ headers.delete 'Content-Length'
33
+ headers.delete 'Content-Type'
34
+ end
35
+
36
+ if drop_body?
37
+ close
38
+ result = []
39
+ end
40
+
41
+ if calculate_content_length?
42
+ # if some other code has already set Content-Length, don't muck with it
43
+ # currently, this would be the static file-handler
44
+ headers['Content-Length'] = calculated_content_length.to_s
45
+ end
46
+
47
+ [status.to_i, headers, result]
48
+ end
49
+
50
+ private
51
+
52
+ def calculate_content_length?
53
+ headers['Content-Type'] && !headers['Content-Length'] && (Array === body)
54
+ end
55
+
56
+ def calculated_content_length
57
+ body.inject(0) { |l, p| l + p.bytesize }
58
+ end
59
+
60
+ def drop_content_info?
61
+ (status.to_i / 100 == 1) || drop_body?
62
+ end
63
+
64
+ def drop_body?
65
+ DROP_BODY_RESPONSES.include?(status.to_i)
66
+ end
67
+ end
68
+ end
data/lib/safrano.rb ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'rexml/document'
5
+ require 'safrano_core.rb'
6
+ require 'odata/entity.rb'
7
+ require 'odata/collection.rb'
8
+ require 'service.rb'
9
+ require 'odata/walker.rb'
10
+ require 'sequel'
11
+ require 'rack_app'
12
+ require 'odata_rack_builder'
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Type mapping DB --> Edm
4
+ module OData
5
+ # TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
6
+ # "STRING" => "Edm.String"}
7
+ # Todo: complete mapping... this is just for the most common ones
8
+
9
+ # TODO: use Sequel GENERIC_TYPES: -->
10
+ # Constants
11
+ # GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime
12
+ # Time File TrueClass FalseClass'.freeze
13
+ # Classes specifying generic types that Sequel will convert to
14
+ # database-specific types.
15
+ def self.get_edm_type(db_type:)
16
+ case db_type
17
+ when 'INTEGER'
18
+ 'Edm.Int32'
19
+ when 'TEXT', 'STRING'
20
+ 'Edm.String'
21
+ else
22
+ 'Edm.String' if /\ACHAR\s*\(\d+\)\z/ =~ db_type
23
+ end
24
+ end
25
+ end
26
+
27
+ module REXML
28
+ # some small extensions
29
+ class Document
30
+ def to_pretty_xml
31
+ formatter = REXML::Formatters::Pretty.new(2)
32
+ formatter.compact = true
33
+ strio = ''
34
+ formatter.write(root, strio)
35
+ strio
36
+ end
37
+ end
38
+ end
39
+
40
+ # Core
41
+ module Safrano
42
+ # represents a state transition when navigating/parsing the url path
43
+ # from left to right
44
+ class Transition < Regexp
45
+ attr_accessor :trans
46
+ attr_accessor :match_result
47
+ attr_accessor :rgx
48
+ def initialize(arg, trans: nil)
49
+ @rgx = if arg.respond_to? :each_char
50
+ Regexp.new(arg)
51
+ else
52
+ arg
53
+ end
54
+ @trans = trans
55
+ end
56
+
57
+ def do_match(str)
58
+ @match_result = @rgx.match(str)
59
+ end
60
+
61
+ # this assumes some implicit rules in the way the regexps are built
62
+ def path_remain
63
+ @match_result[2] if @match_result && @match_result[2]
64
+ end
65
+
66
+ def path_done
67
+ if @match_result
68
+ @match_result[1] || ''
69
+ else
70
+ ''
71
+ end
72
+ end
73
+
74
+ def do_transition(ctx)
75
+ ctx.method(@trans).call(@match_result)
76
+ end
77
+ end
78
+
79
+ TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
80
+ TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
81
+ trans: 'transition_metadata')
82
+ TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
83
+ trans: 'transition_batch')
84
+ TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
85
+ trans: 'transition_count')
86
+ attr_accessor :allowed_transitions
87
+ end
88
+
89
+ # for $count and number type attributes
90
+ # class Fixnum deprecated as of ruby 2.4+
91
+ class Integer
92
+ def odata_get(_req)
93
+ [200, { 'Content-Type' => 'text/plain;charset=utf-8' }, to_s]
94
+ end
95
+ end
96
+
97
+ # for string type attributes
98
+ class String
99
+ def odata_get(_req)
100
+ [200, { 'Content-Type' => 'text/plain;charset=utf-8' }, to_s]
101
+ end
102
+ end
103
+
104
+ # Final attribute value....
105
+ class Object
106
+ def allowed_transitions
107
+ [Safrano::TransitionEnd]
108
+ end
109
+
110
+ def transition_end(_match_result)
111
+ [nil, :end]
112
+ end
113
+ end
data/lib/service.rb ADDED
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rexml/document'
4
+ require 'odata/relations.rb'
5
+ require 'odata/batch.rb'
6
+ require 'odata/error.rb'
7
+
8
+ module OData
9
+ # this module has all methods related to expand/defered output preparation
10
+ # and will be included in Service class
11
+ module ExpandHandler
12
+ def get_deferred_odata_h(entity:, attrib:, uribase:)
13
+ { '__deferred' => { 'uri' => "#{entity.uri(uribase)}/#{attrib}" } }
14
+ end
15
+
16
+ def handle_entity_expand_one(entity:, exp_one:, nav_values_h:, nav_coll_h:,
17
+ uribase:)
18
+ if exp_one.include?('/')
19
+ m = %r{\A(\w+)\/?(.*)\z}.match(exp_one)
20
+ cur_exp = m[1].strip
21
+ rest_exp = m[2]
22
+ # TODO: check errorhandling
23
+ raise OData::ServerError if cur_exp.nil?
24
+
25
+ k = cur_exp.to_sym
26
+ else
27
+ k = exp_one.strip.to_sym
28
+ rest_exp = nil
29
+ end
30
+
31
+ if (enval = entity.nav_values[k])
32
+ nav_values_h[k.to_s] = get_entity_odata_h(entity: enval,
33
+ expand: rest_exp,
34
+ uribase: uribase)
35
+ elsif (encoll = entity.nav_coll[k])
36
+ # nav attributes that are a collection (x..n)
37
+ nav_coll_h[k.to_s] = encoll.map do |xe|
38
+ if xe
39
+ get_entity_odata_h(entity: xe, expand: rest_exp,
40
+ uribase: uribase)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def handle_entity_expand(entity:, expand:, nav_values_h:,
47
+ nav_coll_h:, uribase:)
48
+ expand.strip!
49
+ explist = expand.split(',')
50
+ # handle multiple expands
51
+ explist.each do |exp|
52
+ handle_entity_expand_one(entity: entity, exp_one: exp,
53
+ nav_values_h: nav_values_h,
54
+ nav_coll_h: nav_coll_h,
55
+ uribase: uribase)
56
+ end
57
+ end
58
+
59
+ def handle_entity_deferred_attribs(entity:, nav_values_h:,
60
+ nav_coll_h:, uribase:)
61
+ entity.nav_values.each_key do |ksy|
62
+ ks = ksy.to_s
63
+ next if nav_values_h.key?(ks)
64
+
65
+ nav_values_h[ks] = get_deferred_odata_h(entity: entity,
66
+ attrib: ks, uribase: uribase)
67
+ end
68
+ entity.nav_coll.each_key do |ksy|
69
+ ks = ksy.to_s
70
+ next if nav_coll_h.key?(ks)
71
+
72
+ nav_coll_h[ks] = get_deferred_odata_h(entity: entity, attrib: ks,
73
+ uribase: uribase)
74
+ end
75
+ end
76
+
77
+ def get_entity_odata_h(entity:, expand: nil, uribase:)
78
+ hres = {}
79
+ hres['__metadata'] = entity.metadata_h(uribase: uribase)
80
+ hres.merge!(entity.values)
81
+
82
+ nav_values_h = {}
83
+ nav_coll_h = {}
84
+
85
+ # handle expanded nav attributes
86
+ unless expand.nil?
87
+ handle_entity_expand(entity: entity, expand: expand,
88
+ nav_values_h: nav_values_h,
89
+ nav_coll_h: nav_coll_h,
90
+ uribase: uribase)
91
+ end
92
+
93
+ # handle not expanded (deferred) nav attributes
94
+ # TODO: better design/perf
95
+ handle_entity_deferred_attribs(entity: entity,
96
+ nav_values_h: nav_values_h,
97
+ nav_coll_h: nav_coll_h,
98
+ uribase: uribase)
99
+ # merge ...
100
+ hres.merge!(nav_values_h)
101
+ hres.merge!(nav_coll_h)
102
+
103
+ hres
104
+ end
105
+ end
106
+ end
107
+
108
+ module OData
109
+ # xml namespace constants needed for the output of XML service and
110
+ # and metadata
111
+ module XMLNS
112
+ MSFT_ADO = 'http://schemas.microsoft.com/ado'.freeze
113
+ MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm".freeze
114
+ MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx".freeze
115
+ MSFT_ADO_2007_META = MSFT_ADO + \
116
+ '/2007/08/dataservices/metadata'.freeze
117
+
118
+ W3_2005_ATOM = 'http://www.w3.org/2005/Atom'.freeze
119
+ W3_2007_APP = 'http://www.w3.org/2007/app'.freeze
120
+ end
121
+ end
122
+
123
+ # Link to Model
124
+ module OData
125
+ MAX_DATASERVICE_VERSION = '2'.freeze
126
+ MIN_DATASERVICE_VERSION = '1'.freeze
127
+ include XMLNS
128
+ # Base class for service. Subclass will be for V1, V2 etc...
129
+ class ServiceBase
130
+ XML_PREAMBLE = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>' + \
131
+ "\r\n".freeze
132
+
133
+ # This is just a hash of entity Set Names to the corresponding Class
134
+ # Example
135
+ # Book < Sequel::Model(:book)
136
+ # @entity_set_name = 'books'
137
+ # end
138
+ # ---> @cmap ends up as {'books' => Book }
139
+ attr_reader :cmap
140
+
141
+ # this is just the *sorted* list of the entity classes
142
+ # (ie... @cmap.values.sorted)
143
+ attr_accessor :collections
144
+
145
+ attr_accessor :xtitle
146
+ attr_accessor :xname
147
+ attr_accessor :xnamespace
148
+ attr_accessor :xpath_prefix
149
+ # attr_accessor :xuribase
150
+ attr_accessor :meta
151
+ attr_accessor :batch_handler
152
+ attr_accessor :relman
153
+ # Instance attributes for specialized Version specific Instances
154
+ attr_accessor :v1
155
+ attr_accessor :v2
156
+
157
+ # TODO: more elegant design
158
+ attr_reader :data_service_version
159
+
160
+ def initialize(&block)
161
+ # Warning: if you add attributes here, you shall need add them
162
+ # in copy_attribs_to as well
163
+ # because of the version subclasses that dont use "super" initialise
164
+ # (todo: why not??)
165
+ @meta = ServiceMeta.new(self)
166
+ @batch_handler = OData::Batch::DisabledHandler.new
167
+ @relman = OData::RelationManager.new
168
+ @cmap = {}
169
+ instance_eval(&block) if block_given?
170
+ end
171
+
172
+ DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
173
+ # input is the DataServiceVersion request header string, eg.
174
+ # '2.0;blabla' ---> Version -> 2
175
+ def self.parse_data_service_version(inp)
176
+ m = DATASERVICEVERSION_RGX.match(inp)
177
+ m[1] if m
178
+ end
179
+
180
+ def enable_batch
181
+ @batch_handler = OData::Batch::EnabledHandler.new
182
+ @v1.batch_handler = @batch_handler
183
+ @v2.batch_handler = @batch_handler
184
+ end
185
+
186
+ def enable_v1_service
187
+ @v1 = OData::ServiceV1.new
188
+ copy_attribs_to @v1
189
+ end
190
+
191
+ def enable_v2_service
192
+ @v2 = OData::ServiceV2.new
193
+ copy_attribs_to @v2
194
+ end
195
+
196
+ # public API
197
+ def name(nam)
198
+ @xname = nam
199
+ end
200
+
201
+ def namespace(namsp)
202
+ @xnamespace = namsp
203
+ end
204
+
205
+ def title(tit)
206
+ @xtitle = tit
207
+ end
208
+
209
+ def path_prefix(path_pr)
210
+ @xpath_prefix = path_pr.sub(%r{/\z}, '')
211
+ end
212
+ # end public API
213
+
214
+ def copy_attribs_to(other)
215
+ other.cmap = @cmap
216
+ other.collections = @collections
217
+ other.xtitle = @xtitle
218
+ other.xname = @xname
219
+ other.xnamespace = @xnamespace
220
+ # other.xuribase = @xuribase
221
+ other.xpath_prefix = @xpath_prefix
222
+ other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
223
+ other.relman = @relman
224
+ other.batch_handler = @batch_handler
225
+ other
226
+ end
227
+
228
+ def register_model(modelklass, entity_set_name = nil)
229
+ # check that the provided klass is a Sequel Model
230
+ unless modelklass.is_a? Sequel::Model::ClassMethods
231
+ raise OData::API::ModelNameError, modelklass
232
+ end
233
+
234
+ if modelklass.primary_key.is_a?(Array)
235
+ modelklass.extend OData::EntityClassMultiPK
236
+ modelklass.include OData::EntityMultiPK
237
+ else
238
+ modelklass.extend OData::EntityClassSinglePK
239
+ modelklass.include OData::EntitySinglePK
240
+ end
241
+
242
+ modelklass.prepare_pk
243
+ modelklass.prepare_fields
244
+ esname = (entity_set_name || modelklass).to_s.freeze
245
+ modelklass.instance_eval { @entity_set_name = esname }
246
+ @cmap[esname] = modelklass
247
+ set_collections_sorted(@cmap.values)
248
+ end
249
+
250
+ def publish_model(modelklass, entity_set_name = nil, &block)
251
+ register_model(modelklass, entity_set_name)
252
+ # we need to execute the passed block in a deferred step
253
+ # after all models have been registered (due to rel. dependancies)
254
+ # modelklass.instance_eval(&block) if block_given?
255
+ modelklass.deferred_iblock = block if block_given?
256
+ end
257
+
258
+ def cmap=(imap)
259
+ @cmap = imap
260
+ set_collections_sorted(@cmap.values)
261
+ end
262
+
263
+ # take care of sorting required to match longest first while parsing
264
+ # with base_url_regexp
265
+ # example: CrewMember must be matched before Crew otherwise we get error
266
+ def set_collections_sorted(coll_data)
267
+ @collections = coll_data
268
+ if @collections
269
+ @collections.sort_by! { |klass| klass.entity_set_name.size }.reverse!
270
+ end
271
+ @collections
272
+ end
273
+
274
+ # to be called at end of publishing block to ensure we get the right names
275
+ # and additionally build the list of valid attribute path's used
276
+ # for validation of $orderby or $filter params
277
+ def finalize_publishing
278
+ # build the cmap
279
+ @cmap = {}
280
+ @collections.each do |klass|
281
+ @cmap[klass.entity_set_name] = klass
282
+ end
283
+
284
+ # now that we know all model klasses we can handle relationships
285
+ execute_deferred_iblocks
286
+
287
+ # and finally build the path list
288
+ @collections.each(&:build_attribute_path_list)
289
+ end
290
+
291
+ def execute_deferred_iblocks
292
+ @collections.each do |k|
293
+ k.instance_eval(&k.deferred_iblock) if k.deferred_iblock
294
+ end
295
+ end
296
+
297
+ ## Warning: base_url_regexp depends on '@collections', and this needs to be
298
+ ## evaluated after '@collections' is filled !
299
+ # A regexp matching all allowed base entities (eg product|categories )
300
+ def base_url_regexp
301
+ @collections.map(&:entity_set_name).join('|')
302
+ end
303
+
304
+ include Safrano
305
+ include ExpandHandler
306
+
307
+ def service
308
+ hres = {}
309
+ hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
310
+ hres
311
+ end
312
+
313
+ def service_xml(req)
314
+ doc = REXML::Document.new
315
+ # separator required ? ?
316
+ root = doc.add_element('service', 'xml:base' => req.uribase)
317
+
318
+ root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
319
+ root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
320
+ # this generates the main xmlns attribute
321
+ root.add_namespace(XMLNS::W3_2007_APP)
322
+ wp = root.add_element 'workspace'
323
+
324
+ title = wp.add_element('atom:title')
325
+ title.text = @xtitle
326
+
327
+ @collections.each do |klass|
328
+ col = wp.add_element('collection', 'href' => klass.entity_set_name)
329
+ ct = col.add_element('atom:title')
330
+ ct.text = klass.type_name
331
+ end
332
+
333
+ XML_PREAMBLE + doc.to_pretty_xml
334
+ end
335
+
336
+ def metadata_xml(_req)
337
+ doc = REXML::Document.new
338
+ doc.add_element('edmx:Edmx', 'Version' => '1.0')
339
+ doc.root.add_namespace('xmlns:edmx', XMLNS::MSFT_ADO_2007_EDMX)
340
+ serv = doc.root.add_element('edmx:DataServices',
341
+ 'm:DataServiceVersion' => '1.0')
342
+ # 'm:DataServiceVersion' => "#{self.dataServiceVersion}" )
343
+ # DataServiceVersion: This attribute MUST be in the data service
344
+ # metadata namespace
345
+ # (http://schemas.microsoft.com/ado/2007/08/dataservices) and SHOULD
346
+ # be present on an
347
+ # edmx:DataServices element [MC-EDMX] to indicate the version of the
348
+ # data service CSDL
349
+ # annotations (attributes in the data service metadata namespace) that
350
+ # are used by the document.
351
+ # Consumers of a data-service metadata endpoint ought to first read this
352
+ # attribute value to determine if
353
+ # they can safely interpret all constructs within the document. The
354
+ # value of this attribute MUST be 1.0
355
+ # unless a "FC_KeepInContent" customizable feed annotation
356
+ # (section 2.2.3.7.2.1) with a value equal to
357
+ # false is present in the CSDL document within the edmx:DataServices
358
+ # node. In this case, the
359
+ # attribute value MUST be 2.0 or greater.
360
+ # In the absence of DataServiceVersion, consumers of the CSDL document
361
+ # can assume the highest DataServiceVersion they can handle.
362
+ serv.add_namespace('xmlns:m', XMLNS::MSFT_ADO_2007_META)
363
+
364
+ schema = serv.add_element('Schema',
365
+ 'Namespace' => @xnamespace,
366
+ 'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
367
+ @collections.each do |klass|
368
+ # 1. all EntityType
369
+ enty = schema.add_element('EntityType', 'Name' => klass.to_s)
370
+ # with their properties
371
+ klass.db_schema.each do |pnam, prop|
372
+ if prop[:primary_key] == true
373
+ enty.add_element('Key').add_element('PropertyRef',
374
+ 'Name' => pnam.to_s)
375
+ end
376
+ # attrs = {'Name'=>pnam.to_s,
377
+ # 'Type'=>OData::TypeMap[ prop[:db_type] ]}
378
+ attrs = { 'Name' => pnam.to_s,
379
+ 'Type' => OData.get_edm_type(db_type: prop[:db_type]) }
380
+ attrs['Nullable'] = 'false' if prop[:allow_null] == false
381
+ enty.add_element('Property', attrs)
382
+ end
383
+ # and their Nav attributes == Sequel Model association
384
+ klass.associations.each do |assoc|
385
+ # associated objects need to be in the map...
386
+ next unless @cmap[assoc.to_s]
387
+
388
+ from = klass.type_name
389
+ to = @cmap[assoc.to_s].type_name
390
+
391
+ rel = @relman.get([from, to])
392
+
393
+ # use Sequel reflection to get multiplicity (will be used later
394
+ # in 2. Associations below)
395
+ reflect = klass.association_reflection(assoc)
396
+ case reflect[:type]
397
+ # TODO: use multiplicity 1 when needed instead of '0..1'
398
+ when :one_to_one
399
+ rel.set_multiplicity(from, '0..1')
400
+ rel.set_multiplicity(to, '0..1')
401
+ when :one_to_many
402
+ rel.set_multiplicity(from, '0..1')
403
+ rel.set_multiplicity(to, '*')
404
+ when :many_to_one
405
+ rel.set_multiplicity(from, '*')
406
+ rel.set_multiplicity(to, '0..1')
407
+ when :many_to_many
408
+ rel.set_multiplicity(from, '*')
409
+ rel.set_multiplicity(to, '*')
410
+ end
411
+ # <NavigationProperty Name="Supplier"
412
+ # Relationship="ODataDemo.Product_Supplier_Supplier_Products"
413
+ # FromRole="Product_Supplier" ToRole="Supplier_Products"/>
414
+
415
+ nattrs = { 'Name' => to,
416
+ 'Relationship' => "#{@xnamespace}.#{rel.name}",
417
+ 'FromRole' => from,
418
+ 'ToRole' => to }
419
+ enty.add_element('NavigationProperty', nattrs)
420
+ end
421
+ end
422
+ # 2. Associations
423
+ @relman.each_rel do |rel|
424
+ assoc = schema.add_element('Association', 'Name' => rel.name)
425
+
426
+ rel.each_endobj do |eo|
427
+ assoend = { 'Type' => "#{@xnamespace}.#{eo}",
428
+ 'Role' => eo,
429
+ 'Multiplicity' => rel.multiplicity[eo] }
430
+
431
+ assoc.add_element('End', assoend)
432
+ end
433
+ end
434
+
435
+ # 3. Enty container
436
+ ec = schema.add_element('EntityContainer',
437
+ 'Name' => @xname,
438
+ 'm:IsDefaultEntityContainer' => 'true')
439
+ @collections.each do |klass|
440
+ # 3.a Entity set's
441
+ ec.add_element('EntitySet',
442
+ 'Name' => klass.entity_set_name,
443
+ 'EntityType' => "#{@xnamespace}.#{klass.type_name}")
444
+ end
445
+ # 3.b Association set's
446
+ @relman.each_rel do |rel|
447
+ assoc = ec.add_element('AssociationSet',
448
+ 'Name' => rel.name,
449
+ 'Association' => "#{@xnamespace}.#{rel.name}")
450
+
451
+ rel.each_endobj do |eo|
452
+ clazz = Object.const_get(eo)
453
+ assoend = { 'EntitySet' => clazz.entity_set_name.to_s, 'Role' => eo }
454
+ assoc.add_element('End', assoend)
455
+ end
456
+ end
457
+ XML_PREAMBLE + doc.to_pretty_xml
458
+ end
459
+
460
+ # methods related to transitions to next state (cf. walker)
461
+ module Transitions
462
+ def allowed_transitions
463
+ @allowed_transitions = [
464
+ Safrano::TransitionEnd,
465
+ Safrano::TransitionMetadata,
466
+ Safrano::TransitionBatch,
467
+ Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
468
+ trans: 'transition_collection')
469
+ ]
470
+ end
471
+
472
+ def transition_collection(match_result)
473
+ [@cmap[match_result[1]], :run] if match_result[1]
474
+ end
475
+
476
+ def transition_batch(_match_result)
477
+ [@batch_handler, :run]
478
+ end
479
+
480
+ def transition_metadata(_match_result)
481
+ [@meta, :run]
482
+ end
483
+
484
+ def transition_end(_match_result)
485
+ [nil, :end]
486
+ end
487
+ end
488
+
489
+ include Transitions
490
+
491
+ def odata_get(req)
492
+ if req.accept?('application/xml')
493
+ # app.headers 'Content-Type' => 'application/xml;charset=utf-8'
494
+ # Doc: 2.2.3.7.1 Service Document As per [RFC5023], AtomPub Service
495
+ # Documents MUST be
496
+ # identified with the "application/atomsvc+xml" media type (see
497
+ # [RFC5023] section 8).
498
+ [200, { 'Content-Type' => 'application/atomsvc+xml;charset=utf-8' },
499
+ service_xml(req)]
500
+ else
501
+ # TODO: other formats ?
502
+ # this is returned by http://services.odata.org/V2/OData/OData.svc
503
+ 415
504
+ end
505
+ end
506
+ end
507
+
508
+ # for OData V1
509
+ class ServiceV1 < ServiceBase
510
+ def initialize
511
+ @data_service_version = '1.0'
512
+ end
513
+
514
+ def get_coll_odata_h(array:, expand: nil, uribase:)
515
+ array.map do |w|
516
+ get_entity_odata_h(entity: w,
517
+ expand: expand,
518
+ uribase: uribase)
519
+ end
520
+ end
521
+
522
+ def get_emptycoll_odata_h
523
+ [{}]
524
+ end
525
+ end
526
+
527
+ # for OData V2
528
+ class ServiceV2 < ServiceBase
529
+ def initialize
530
+ @data_service_version = '2.0'
531
+ end
532
+
533
+ def get_coll_odata_h(array:, expand: nil, uribase:)
534
+ { 'results' =>
535
+ array.map do |w|
536
+ get_entity_odata_h(entity: w,
537
+ expand: expand,
538
+ uribase: uribase)
539
+ end }
540
+ end
541
+
542
+ def get_emptycoll_odata_h
543
+ { 'results' => [{}] }
544
+ end
545
+ end
546
+
547
+ # a virtual entity for the service metadata
548
+ class ServiceMeta
549
+ attr_accessor :service
550
+ def initialize(service)
551
+ @service = service
552
+ end
553
+
554
+ def allowed_transitions
555
+ @allowed_transitions = [Safrano::Transition.new('',
556
+ trans: 'transition_end')]
557
+ end
558
+
559
+ def transition_end(_match_result)
560
+ [nil, :end]
561
+ end
562
+
563
+ def odata_get(req)
564
+ if req.accept?('application/xml')
565
+ [200, { 'Content-Type' => 'application/xml;charset=utf-8' },
566
+ @service.metadata_xml(req)]
567
+ else # TODO: other formats
568
+ 415
569
+ end
570
+ end
571
+ end
572
+ end
573
+ # end of Module OData
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: safrano
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - D.M.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-04-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.15'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.51'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.51'
69
+ description: Safrano is a small experimental OData server framework based on Ruby,
70
+ Rack and Sequel.
71
+ email: dm@0data.dev
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/odata_rack_builder.rb
77
+ - lib/rack_app.rb
78
+ - lib/request.rb
79
+ - lib/response.rb
80
+ - lib/safrano.rb
81
+ - lib/safrano_core.rb
82
+ - lib/service.rb
83
+ homepage: https://gitlab.com/dm0da/safrano
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubygems_version: 3.0.3
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Safrano
106
+ test_files: []