safrano 0.0.1
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.
- 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: []
|