safrano 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/odata_rack_builder.rb +20 -0
- data/lib/rack_app.rb +163 -0
- data/lib/request.rb +158 -0
- data/lib/response.rb +68 -0
- data/lib/safrano.rb +12 -0
- data/lib/safrano_core.rb +113 -0
- data/lib/service.rb +573 -0
- metadata +106 -0
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'
|
data/lib/safrano_core.rb
ADDED
@@ -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: []
|