medidata 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/medidata.rb +3 -0
- data/lib/medidata/api.rb +10 -0
- data/lib/medidata/api/client.rb +198 -0
- data/lib/medidata/api/download.rb +29 -0
- data/lib/medidata/api/error.rb +13 -0
- data/lib/medidata/api/http.rb +363 -0
- data/lib/medidata/api/notification.rb +39 -0
- data/lib/medidata/api/participant.rb +47 -0
- data/lib/medidata/api/type.rb +8 -0
- data/lib/medidata/api/upload.rb +88 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 12a82b143cd97a12a83f592971730feca9678a498aceef53671a919f6f3a6413
|
4
|
+
data.tar.gz: f4dadff58258bed1a7f6cb9f16dcd0af6a2ee8aae08a698a6acdec048570024f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b9af6ed6ceb4d866e0b806c6686bf40ff35155964c0f24871bdbfbfa5eab2eab65e55b11a5ebd6295fbd888afc7ffa49ed2ab5791307e8b55da6b28fa94156f0
|
7
|
+
data.tar.gz: 84bb1fc78121f31c1b237f939dcf0f817c84153968492cc8f31748e86463f65f80976539334ecbab9fb7d2208a255602ccf1036027bfb0a2a3d6108c741b8dc8
|
data/lib/medidata.rb
ADDED
data/lib/medidata/api.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
require 'medidata/api/error'
|
3
|
+
require 'medidata/api/http'
|
4
|
+
require 'medidata/api/type'
|
5
|
+
require 'medidata/api/upload'
|
6
|
+
require 'medidata/api/download'
|
7
|
+
require 'medidata/api/participant'
|
8
|
+
require 'medidata/api/notification'
|
9
|
+
require 'medidata/api/client'
|
10
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
require 'json'
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
require_relative "error"
|
6
|
+
require_relative "http"
|
7
|
+
|
8
|
+
require_relative "upload"
|
9
|
+
require_relative "participant"
|
10
|
+
require_relative "notification"
|
11
|
+
|
12
|
+
class Client
|
13
|
+
# * *Args* :
|
14
|
+
# - +host+ -> REST API client
|
15
|
+
# - +lang+ -> Preferred language for error messages
|
16
|
+
def initialize(host:, path_prefix: '', lang: 'de')
|
17
|
+
@host = host
|
18
|
+
@lang = lang
|
19
|
+
rest = REST.new(
|
20
|
+
host: host,
|
21
|
+
url_path: [path_prefix],
|
22
|
+
request_headers: {
|
23
|
+
"Accept": "application/json",
|
24
|
+
"Accept-Language": lang,
|
25
|
+
"DenteoSecret": "yes_it's_us"
|
26
|
+
},
|
27
|
+
)
|
28
|
+
|
29
|
+
@rest = rest
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set credentials for this Client's instance
|
33
|
+
#
|
34
|
+
# * *Args* :
|
35
|
+
# - +id+ -> Medidata client ID
|
36
|
+
# - +username+ -> Username
|
37
|
+
# - +password+ -> password
|
38
|
+
def auth(id, username, password)
|
39
|
+
@rest.update_headers({
|
40
|
+
"X-CLIENT-ID": id,
|
41
|
+
"Authorization": "Basic " + Base64.encode64(username + ":" + password).strip
|
42
|
+
})
|
43
|
+
end
|
44
|
+
|
45
|
+
# * *Args* :
|
46
|
+
# - +limit+ -> The total number of participants allowed
|
47
|
+
# - +offset+ -> Pagination offset
|
48
|
+
# - +query+ -> ParticipantsQuery object (optional)
|
49
|
+
def participants(limit: 100, offset: 0, query: nil)
|
50
|
+
qs = {
|
51
|
+
"limit": limit,
|
52
|
+
"offset": offset
|
53
|
+
}
|
54
|
+
if query
|
55
|
+
qs["lawtype"] = query.lawType if query.lawType
|
56
|
+
qs["name"] = query.name if query.name
|
57
|
+
qs["glnparticipant"] = query.glnParticipant if query.glnParticipant
|
58
|
+
end
|
59
|
+
|
60
|
+
params = {
|
61
|
+
"query_params": qs
|
62
|
+
}
|
63
|
+
res = @rest.ela.participants.get(params)
|
64
|
+
case res.status_code
|
65
|
+
when 200 then
|
66
|
+
body = res.parsed_body
|
67
|
+
raise "Invalid participants response body" unless body.kind_of?(Array)
|
68
|
+
body.map { |row| Medidata::API::Participant.new(row) }
|
69
|
+
when 400 then
|
70
|
+
raise BadRequestError.new("bad request")
|
71
|
+
when 401 then
|
72
|
+
raise UnauthenticatedError.new("authentication required to load participants")
|
73
|
+
when 403 then
|
74
|
+
raise PermissionError.new("wrong credentials to load participants")
|
75
|
+
else
|
76
|
+
raise "Error fetching Medidata participants <#{res.status_code}>"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Upload a document to Medidata
|
81
|
+
#
|
82
|
+
# * *Args* :
|
83
|
+
# - +file+ -> File to upload (e.g. Invoice XML)
|
84
|
+
# - +info+ -> Medidata::API::UploadControlData (optional)
|
85
|
+
def upload(file, info = nil)
|
86
|
+
multipart = Medidata::API::MultipartPost.new
|
87
|
+
multipart.with_binary key: "elauploadstream", value: file
|
88
|
+
multipart.with_text key: "elauploadinfo", value: Medidata::API::UploadControlData.new(info).to_json if info
|
89
|
+
|
90
|
+
bounday = "__MEDI_REST_IN_PEACE__"
|
91
|
+
params = {
|
92
|
+
"request_headers": {
|
93
|
+
"Content-Type": "multipart/form-data; charset=utf-8; boundary=#{bounday}"
|
94
|
+
},
|
95
|
+
"request_body": multipart.build(bounday)
|
96
|
+
}
|
97
|
+
res = @rest.ela.uploads.post(params)
|
98
|
+
case res.status_code
|
99
|
+
when 201 then
|
100
|
+
# Upload created
|
101
|
+
Medidata::API::Upload.new(res.parsed_body)
|
102
|
+
when 400 then
|
103
|
+
raise BadRequestError.new("bad request")
|
104
|
+
when 401 then
|
105
|
+
raise UnauthenticatedError.new("authentication required to upload")
|
106
|
+
when 403 then
|
107
|
+
raise PermissionError.new("wrong credentials to upload")
|
108
|
+
when 409 then
|
109
|
+
# TODO: Extract transmission reference from response
|
110
|
+
raise ConflictError.new("this file has already been uploaded")
|
111
|
+
else
|
112
|
+
raise "Error uploading to Medidata <#{res.status_code}>"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Check upload status
|
117
|
+
#
|
118
|
+
# * *Args* :
|
119
|
+
# - +tref+ -> Upload's transmission reference
|
120
|
+
def upload_status(tref)
|
121
|
+
res = @rest.ela.uploads._(tref).status.get
|
122
|
+
case res.status_code
|
123
|
+
when 200 then
|
124
|
+
Medidata::API::Upload.new(res.parsed_body)
|
125
|
+
when 400 then
|
126
|
+
raise BadRequestError.new("bad request")
|
127
|
+
when 401 then
|
128
|
+
raise UnauthenticatedError.new("authentication required to check upload status")
|
129
|
+
when 403 then
|
130
|
+
raise PermissionError.new("wrong credentials to check upload status")
|
131
|
+
when 404 then
|
132
|
+
raise MissingError.new("upload status not found")
|
133
|
+
else
|
134
|
+
raise "Error fetching Medidata upload status <#{res.status_code}>"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Load pending notifications
|
139
|
+
#
|
140
|
+
# * *Args* :
|
141
|
+
# - +limit+ -> The total number of notifications allowed
|
142
|
+
# - +offset+ -> Pagination offset
|
143
|
+
def notifications_pending(limit: 100, offset: 0)
|
144
|
+
params = {
|
145
|
+
"query_params": {
|
146
|
+
"limit": limit,
|
147
|
+
"offset": offset
|
148
|
+
}
|
149
|
+
}
|
150
|
+
res = @rest.ela.notifications.get(params)
|
151
|
+
case res.status_code
|
152
|
+
when 200 then
|
153
|
+
body = res.parsed_body
|
154
|
+
raise "Invalid notifications response body" unless body.kind_of?(Array)
|
155
|
+
body.map { |row| Medidata::API::Notification.new(row) }
|
156
|
+
when 400 then
|
157
|
+
raise BadRequestError.new("bad request")
|
158
|
+
when 401 then
|
159
|
+
raise UnauthenticatedError.new("authentication required to load pending notifications")
|
160
|
+
when 403 then
|
161
|
+
raise PermissionError.new("wrong credentials to load pending notifications")
|
162
|
+
else
|
163
|
+
raise "Error fetching Medidata notifications <#{res.status_code}>"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Confirm notification receipt
|
168
|
+
#
|
169
|
+
# All notifications received from Medidata are pending by default and
|
170
|
+
# clients have to manually mark them as "read". That should be done
|
171
|
+
# on a regular basis according to Medidata.
|
172
|
+
#
|
173
|
+
# * *Args* :
|
174
|
+
# - +id+ -> Notification unique identifier
|
175
|
+
def notifications_confirm_receipt(id)
|
176
|
+
params = {
|
177
|
+
"request_body": {
|
178
|
+
"notificationFetched": true
|
179
|
+
}
|
180
|
+
}
|
181
|
+
res = @rest.ela.notifications._(id).status.put(params)
|
182
|
+
case res.status_code
|
183
|
+
when 200, 204 then
|
184
|
+
true
|
185
|
+
when 400 then
|
186
|
+
raise BadRequestError.new("bad request")
|
187
|
+
when 401 then
|
188
|
+
raise UnauthenticatedError.new("authentication required to confirm notification receipt")
|
189
|
+
when 403 then
|
190
|
+
raise PermissionError.new("wrong credentials to confirm notification receipt")
|
191
|
+
when 404 then
|
192
|
+
raise MissingError.new("notification not found")
|
193
|
+
else
|
194
|
+
raise "Error updating notification status <#{res.status_code}>"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
require_relative "type"
|
3
|
+
|
4
|
+
# A Download is available when an invoice has been sent to the insurance company,
|
5
|
+
# but the content check does not go through there and an invoice response
|
6
|
+
# (General Invoice Response) is returned.
|
7
|
+
# These invoice responses then arrive on the MD client and have to be downloaded
|
8
|
+
# and confirmed by the customer's Denteo application using ela / downloads.
|
9
|
+
class Download < Dry::Struct
|
10
|
+
# Reference to the document. This is generated by the MediData client.
|
11
|
+
attribute :transmissionReference, Types::Strict::String
|
12
|
+
# Reference to the document
|
13
|
+
attribute :documentReference, Types::Strict::String
|
14
|
+
# Reference to the business case
|
15
|
+
attribute :correlationReference, Types::Strict::String
|
16
|
+
# Sender identification
|
17
|
+
attribute :senderGln, Types::Strict::String
|
18
|
+
# MIME document type
|
19
|
+
attribute :docType, Types::Strict::String
|
20
|
+
# Length of the document in bytes
|
21
|
+
attribute :fileSize, Types::Strict::Integer
|
22
|
+
# TEST or PRODUCTION
|
23
|
+
attribute :mode, Types::Strict::String
|
24
|
+
# Document status
|
25
|
+
attribute :status, Types::Strict::String
|
26
|
+
# Date document was delivered
|
27
|
+
attribute :created, Types::JSON::DateTime
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
class MissingError < StandardError
|
3
|
+
end
|
4
|
+
class BadRequestError < StandardError
|
5
|
+
end
|
6
|
+
class PermissionError < StandardError
|
7
|
+
end
|
8
|
+
class UnauthenticatedError < StandardError
|
9
|
+
end
|
10
|
+
class ConflictError < StandardError
|
11
|
+
# TODO: Add resource
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,363 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/https'
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
# Holds the response from an API call.
|
8
|
+
class Response
|
9
|
+
# Provide useful functionality around API rate limiting.
|
10
|
+
class Ratelimit
|
11
|
+
attr_reader :limit, :remaining, :reset
|
12
|
+
|
13
|
+
# * *Args* :
|
14
|
+
# - +limit+ -> The total number of requests allowed within a rate limit window
|
15
|
+
# - +remaining+ -> The number of requests that have been processed within this current rate limit window
|
16
|
+
# - +reset+ -> The time (in seconds since Unix Epoch) when the rate limit will reset
|
17
|
+
def initialize(limit, remaining, reset)
|
18
|
+
@limit = limit.to_i
|
19
|
+
@remaining = remaining.to_i
|
20
|
+
@reset = Time.at reset.to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
def exceeded?
|
24
|
+
remaining <= 0
|
25
|
+
end
|
26
|
+
|
27
|
+
# * *Returns* :
|
28
|
+
# - The number of requests that have been used out of this
|
29
|
+
# rate limit window
|
30
|
+
def used
|
31
|
+
limit - remaining
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sleep until the reset time arrives. If given a block, it will
|
35
|
+
# be called after sleeping is finished.
|
36
|
+
#
|
37
|
+
# * *Returns* :
|
38
|
+
# - The amount of time (in seconds) that the rate limit slept
|
39
|
+
# for.
|
40
|
+
def wait!
|
41
|
+
now = Time.now.utc.to_i
|
42
|
+
duration = (reset.to_i - now) + 1
|
43
|
+
|
44
|
+
sleep duration if duration >= 0
|
45
|
+
|
46
|
+
yield if block_given?
|
47
|
+
|
48
|
+
duration
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# * *Args* :
|
53
|
+
# - +response+ -> A NET::HTTP response object
|
54
|
+
#
|
55
|
+
attr_reader :status_code, :body, :headers
|
56
|
+
|
57
|
+
def initialize(response)
|
58
|
+
@status_code = response.code.to_i
|
59
|
+
@body = response.body
|
60
|
+
@headers = response.to_hash
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the body as a hash
|
64
|
+
#
|
65
|
+
def parsed_body
|
66
|
+
@parsed_body ||= JSON.parse(@body, symbolize_names: true)
|
67
|
+
end
|
68
|
+
|
69
|
+
def ratelimit
|
70
|
+
return @ratelimit unless @ratelimit.nil?
|
71
|
+
|
72
|
+
limit = headers['X-RateLimit-Limit']
|
73
|
+
remaining = headers['X-RateLimit-Remaining']
|
74
|
+
reset = headers['X-RateLimit-Reset']
|
75
|
+
|
76
|
+
# Guard against possibility that one (or probably, all) of the
|
77
|
+
# needed headers were not returned.
|
78
|
+
@ratelimit = Ratelimit.new(limit, remaining, reset) if limit && remaining && reset
|
79
|
+
|
80
|
+
@ratelimit
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# A simple REST client.
|
85
|
+
class REST
|
86
|
+
attr_reader :host, :request_headers, :url_path, :request, :http
|
87
|
+
# * *Args* :
|
88
|
+
# - +host+ -> Base URL for the api. (e.g. https://api.sendgrid.com)
|
89
|
+
# - +request_headers+ -> A hash of the headers you want applied on
|
90
|
+
# all calls
|
91
|
+
# (e.g. client._("/v3"))
|
92
|
+
# - +url_path+ -> A list of the url path segments
|
93
|
+
# - +proxy_options+ -> A hash of proxy settings.
|
94
|
+
# (e.g. { host: '127.0.0.1', port: 8080 })
|
95
|
+
#
|
96
|
+
def initialize(host: nil, request_headers: nil, url_path: nil, http_options: {}, proxy_options: {}) # rubocop:disable Metrics/ParameterLists
|
97
|
+
@host = host
|
98
|
+
@request_headers = request_headers || {}
|
99
|
+
@url_path = url_path || []
|
100
|
+
@methods = %w[delete get patch post put]
|
101
|
+
@query_params = nil
|
102
|
+
@request_body = nil
|
103
|
+
@http_options = http_options
|
104
|
+
@proxy_options = proxy_options
|
105
|
+
end
|
106
|
+
|
107
|
+
# Update the headers for the request
|
108
|
+
#
|
109
|
+
# * *Args* :
|
110
|
+
# - +request_headers+ -> Hash of request header key/values
|
111
|
+
#
|
112
|
+
def update_headers(request_headers)
|
113
|
+
@request_headers.merge!(request_headers)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Build the final request headers
|
117
|
+
#
|
118
|
+
# * *Args* :
|
119
|
+
# - +request+ -> HTTP::NET request object
|
120
|
+
# * *Returns* :
|
121
|
+
# - HTTP::NET request object
|
122
|
+
#
|
123
|
+
def build_request_headers(request)
|
124
|
+
@request_headers.each do |key, value|
|
125
|
+
request[key] = value
|
126
|
+
end
|
127
|
+
request
|
128
|
+
end
|
129
|
+
|
130
|
+
# Add query parameters to the url
|
131
|
+
#
|
132
|
+
# * *Args* :
|
133
|
+
# - +url+ -> path to endpoint
|
134
|
+
# - +query_params+ -> hash of query parameters
|
135
|
+
# * *Returns* :
|
136
|
+
# - The url string with the query parameters appended
|
137
|
+
#
|
138
|
+
def build_query_params(url, query_params)
|
139
|
+
params = URI.encode_www_form(query_params)
|
140
|
+
url.concat("?#{params}")
|
141
|
+
end
|
142
|
+
|
143
|
+
# Set the query params, request headers and request body
|
144
|
+
#
|
145
|
+
# * *Args* :
|
146
|
+
# - +args+ -> array of args obtained from method_missing
|
147
|
+
#
|
148
|
+
def build_args(args)
|
149
|
+
args.each do |arg|
|
150
|
+
arg.each do |key, value|
|
151
|
+
case key.to_s
|
152
|
+
when 'query_params'
|
153
|
+
@query_params = value
|
154
|
+
when 'request_headers'
|
155
|
+
update_headers(value)
|
156
|
+
when 'request_body'
|
157
|
+
@request_body = value
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Build the final url
|
164
|
+
#
|
165
|
+
# * *Args* :
|
166
|
+
# - +query_params+ -> A hash of query parameters
|
167
|
+
# * *Returns* :
|
168
|
+
# - The final url string
|
169
|
+
#
|
170
|
+
def build_url(query_params: nil)
|
171
|
+
url = @url_path.join('/')
|
172
|
+
url = build_query_params(url, query_params) if query_params
|
173
|
+
URI.parse([@host, url].join('/'))
|
174
|
+
end
|
175
|
+
|
176
|
+
# Build the API request for HTTP::NET
|
177
|
+
#
|
178
|
+
# * *Args* :
|
179
|
+
# - +name+ -> method name, received from method_missing
|
180
|
+
# - +args+ -> args passed to the method
|
181
|
+
# * *Returns* :
|
182
|
+
# - A Response object from make_request
|
183
|
+
#
|
184
|
+
def build_request(name, args)
|
185
|
+
build_args(args) if args
|
186
|
+
# build the request & http object
|
187
|
+
build_http_request(name)
|
188
|
+
# set the content type & request body
|
189
|
+
build_request_body(name)
|
190
|
+
make_request(@http, @request)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Make the API call and return the response. This is separated into
|
194
|
+
# it's own function, so we can mock it easily for testing.
|
195
|
+
#
|
196
|
+
# * *Args* :
|
197
|
+
# - +http+ -> NET:HTTP request object
|
198
|
+
# - +request+ -> NET::HTTP request object
|
199
|
+
# * *Returns* :
|
200
|
+
# - Response object
|
201
|
+
#
|
202
|
+
def make_request(http, request)
|
203
|
+
response = http.request(request)
|
204
|
+
Response.new(response)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Build HTTP request object
|
208
|
+
#
|
209
|
+
# * *Returns* :
|
210
|
+
# - Request object
|
211
|
+
def build_http(host, port)
|
212
|
+
params = [host, port]
|
213
|
+
params += @proxy_options.values_at(:host, :port, :user, :pass) unless @proxy_options.empty?
|
214
|
+
http = add_ssl(Net::HTTP.new(*params))
|
215
|
+
http = add_http_options(http) unless @http_options.empty?
|
216
|
+
http
|
217
|
+
end
|
218
|
+
|
219
|
+
# Allow for https calls
|
220
|
+
#
|
221
|
+
# * *Args* :
|
222
|
+
# - +http+ -> HTTP::NET object
|
223
|
+
# * *Returns* :
|
224
|
+
# - HTTP::NET object
|
225
|
+
#
|
226
|
+
def add_ssl(http)
|
227
|
+
if host.start_with?('https')
|
228
|
+
http.use_ssl = true
|
229
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
230
|
+
end
|
231
|
+
http
|
232
|
+
end
|
233
|
+
|
234
|
+
# Add others http options to http object
|
235
|
+
#
|
236
|
+
# * *Args* :
|
237
|
+
# - +http+ -> HTTP::NET object
|
238
|
+
# * *Returns* :
|
239
|
+
# - HTTP::NET object
|
240
|
+
#
|
241
|
+
def add_http_options(http)
|
242
|
+
@http_options.each do |attribute, value|
|
243
|
+
http.send("#{attribute}=", value)
|
244
|
+
end
|
245
|
+
http
|
246
|
+
end
|
247
|
+
|
248
|
+
# Add variable values to the url.
|
249
|
+
# (e.g. /your/api/{variable_value}/call)
|
250
|
+
# Another example: if you have a ruby reserved word, such as true,
|
251
|
+
# in your url, you must use this method.
|
252
|
+
#
|
253
|
+
# * *Args* :
|
254
|
+
# - +name+ -> Name of the url segment
|
255
|
+
# * *Returns* :
|
256
|
+
# - REST object
|
257
|
+
#
|
258
|
+
def _(name = nil)
|
259
|
+
url_path = name ? @url_path + [name] : @url_path
|
260
|
+
REST.new(
|
261
|
+
host: @host,
|
262
|
+
request_headers: @request_headers,
|
263
|
+
url_path: url_path,
|
264
|
+
http_options: @http_options
|
265
|
+
)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Dynamically add segments to the url, then call a method.
|
269
|
+
# (e.g. client.name.name.get())
|
270
|
+
#
|
271
|
+
# * *Args* :
|
272
|
+
# - The args are automatically passed in
|
273
|
+
# * *Returns* :
|
274
|
+
# - REST object or Response object
|
275
|
+
#
|
276
|
+
# rubocop:disable Style/MethodMissingSuper
|
277
|
+
# rubocop:disable Style/MissingRespondToMissing
|
278
|
+
def method_missing(name, *args, &_block)
|
279
|
+
# We have reached the end of the method chain, make the API call
|
280
|
+
return build_request(name, args) if @methods.include?(name.to_s)
|
281
|
+
|
282
|
+
# Add a segment to the URL
|
283
|
+
_(name)
|
284
|
+
end
|
285
|
+
|
286
|
+
private
|
287
|
+
|
288
|
+
def build_http_request(http_method)
|
289
|
+
uri = build_url(query_params: @query_params)
|
290
|
+
net_http = Kernel.const_get('Net::HTTP::' + http_method.to_s.capitalize)
|
291
|
+
|
292
|
+
@http = build_http(uri.host, uri.port)
|
293
|
+
@request = build_request_headers(net_http.new(uri.request_uri))
|
294
|
+
end
|
295
|
+
|
296
|
+
def build_request_body(http_method)
|
297
|
+
if @request_body && is_json_request? && [Hash, Array].include?(@request_body.class)
|
298
|
+
@request.body = @request_body.to_json
|
299
|
+
@request['Content-Type'] = 'application/json'
|
300
|
+
elsif !@request_body && http_method.to_s != 'get'
|
301
|
+
@request['Content-Type'] = ''
|
302
|
+
else
|
303
|
+
@request['Content-Type'] = @request_headers[:'Content-Type']
|
304
|
+
@request.body = @request_body
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def is_json_request?
|
309
|
+
!@request_headers.key?(:'Content-Type') || @request_headers[:'Content-Type'] == 'application/json'
|
310
|
+
end
|
311
|
+
# rubocop:enable Style/MethodMissingSuper
|
312
|
+
# rubocop:enable Style/MissingRespondToMissing
|
313
|
+
end
|
314
|
+
|
315
|
+
class MultipartPost
|
316
|
+
EOL = "\r\n"
|
317
|
+
|
318
|
+
def initialize
|
319
|
+
@params = Array.new
|
320
|
+
end
|
321
|
+
|
322
|
+
def with_text(key:, value:)
|
323
|
+
@params << multipart_text(key, value)
|
324
|
+
end
|
325
|
+
|
326
|
+
def with_binary(key:, value:)
|
327
|
+
@params << multipart_binary(key, value)
|
328
|
+
end
|
329
|
+
|
330
|
+
def with_file(key:, value:, filename:, mime_type:)
|
331
|
+
@params << multipart_file(key, value, filename, mime_type)
|
332
|
+
end
|
333
|
+
|
334
|
+
def build(bounday)
|
335
|
+
body = @params.map{|p| "--#{bounday}#{EOL}" << p}.join ""
|
336
|
+
body << "#{EOL}--#{bounday}--#{EOL}"
|
337
|
+
end
|
338
|
+
|
339
|
+
private
|
340
|
+
|
341
|
+
def multipart_text(key, value)
|
342
|
+
"Content-Disposition: form-data; name=\"#{key}\"" <<
|
343
|
+
EOL <<
|
344
|
+
EOL <<
|
345
|
+
"#{value}" << EOL
|
346
|
+
end
|
347
|
+
|
348
|
+
def multipart_file(key, value, filename, mime_type)
|
349
|
+
"Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"#{EOL}" <<
|
350
|
+
"Content-Type: #{mime_type}#{EOL}" <<
|
351
|
+
EOL <<
|
352
|
+
"#{value}" << EOL
|
353
|
+
end
|
354
|
+
|
355
|
+
def multipart_binary(key, value)
|
356
|
+
"Content-Disposition: form-data; name=\"#{key}\"#{EOL}" <<
|
357
|
+
"Content-Transfer-Encoding: binary#{EOL}" <<
|
358
|
+
"Content-Type: application/octet-stream#{EOL}" <<
|
359
|
+
EOL <<
|
360
|
+
"#{value}" << EOL
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
require_relative "type"
|
3
|
+
|
4
|
+
# A Notification is generated when an upload results in the status «ERROR»,
|
5
|
+
# e.g. the schema check on the MD client detects an error.
|
6
|
+
#
|
7
|
+
# The status ERROR indicates that there was an error during the transmission
|
8
|
+
# and the recipient did not receive the file.
|
9
|
+
# In the associated notification of this ERROR, the customer receives more
|
10
|
+
# detailed information on the error code, cause and possible remedial measures.
|
11
|
+
class Notification < Dry::Struct
|
12
|
+
# ID is a unique identifier for notification
|
13
|
+
attribute :id, Types::Strict::Integer.optional
|
14
|
+
# Subject of the notification (in three languages)
|
15
|
+
attribute :subject, Types::Strict::Hash
|
16
|
+
# The content of the notification (in three languages)
|
17
|
+
attribute :message, Types::Strict::Hash
|
18
|
+
# The notification type.
|
19
|
+
attribute :type, Types::Strict::String.optional.default("".freeze)
|
20
|
+
# “true” if the notification has already been marked as red, otherwise “false”.
|
21
|
+
attribute :read, Types::Strict::Bool.default(false)
|
22
|
+
# The notification’s creation date ISO 8601 timestamp
|
23
|
+
attribute :created, Types::JSON::DateTime
|
24
|
+
# The template’s name
|
25
|
+
attribute :template, Types::Strict::String
|
26
|
+
# The notification’s mode
|
27
|
+
attribute :mode, Types::Strict::String
|
28
|
+
# The error code
|
29
|
+
attribute :errorCode, Types::Strict::String
|
30
|
+
# Possible causes (in three languages). Texts are entered by MediData
|
31
|
+
attribute :potentialReasons, Types::Strict::Hash
|
32
|
+
# Possible solutionss (in three languages). Texts are entered by MediData
|
33
|
+
attribute :possibleSolutions, Types::Strict::Hash
|
34
|
+
# Additional parameters for notification
|
35
|
+
attribute :notificationParameters, Types::Strict::Hash
|
36
|
+
# Additional technical information. (can be used for problem analysis.
|
37
|
+
attribute :technicalInformation, Types::Strict::String.optional.default(nil)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
require_relative "type"
|
3
|
+
|
4
|
+
class Participant < Dry::Struct
|
5
|
+
# Recipient’s GLN number.
|
6
|
+
# Note: Always the same GLN as with the parameter “glnReceiver”.
|
7
|
+
attribute :glnParticipant, Types::Strict::String
|
8
|
+
# Recipient’s GLN number
|
9
|
+
attribute :glnReceiver, Types::Strict::String
|
10
|
+
# Name of the participant
|
11
|
+
attribute :name, Types::Strict::String
|
12
|
+
# Address (street name + house number)
|
13
|
+
attribute :street, Types::Strict::String
|
14
|
+
# Post code
|
15
|
+
attribute :zipCode, Types::Strict::String
|
16
|
+
# Name of city
|
17
|
+
attribute :town, Types::Strict::String
|
18
|
+
# List of laws supported by the participants
|
19
|
+
attribute :lawTypes, Types::Strict::Array
|
20
|
+
# Number registered with the Swiss Federal Office of Public Health (FOPH)
|
21
|
+
attribute :bagNumber, Types::Strict::Integer.optional
|
22
|
+
# Is the change from TG to TP accepted by the participant. The default is false.
|
23
|
+
attribute :tgTpChange, Types::Strict::Bool
|
24
|
+
# The sending of a document to this participant may result in additional
|
25
|
+
# costs (e.g. printing costs) if additionalCosts is set to true. The default is false.
|
26
|
+
attribute :additionalCosts, Types::Strict::Bool
|
27
|
+
# Maximum document size that the participant can or wants to receive.
|
28
|
+
attribute :maxReceive, Types::Strict::Integer
|
29
|
+
# List of the document MIME types not supported by the participant.
|
30
|
+
# No documents of this type may be sent to the recipient.
|
31
|
+
attribute :notSupported, Types::Strict::Array
|
32
|
+
# Specifies whether the participant can receive and process TG documents.
|
33
|
+
attribute :tgAllowed, Types::Strict::Bool
|
34
|
+
end
|
35
|
+
|
36
|
+
class ParticipantsQuery
|
37
|
+
attr_reader :lawType, :name, :glnParticipant
|
38
|
+
|
39
|
+
def initialize(lawType: "", name: "", glnParticipant: "")
|
40
|
+
@lawType = lawType
|
41
|
+
@name = name
|
42
|
+
@glnParticipant = glnParticipant
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: Add LawType type
|
47
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Medidata::API
|
2
|
+
require_relative "type"
|
3
|
+
|
4
|
+
# Upload: is generally used for sending payloads in XML format, i.e. sending the invoice.
|
5
|
+
class Upload < Dry::Struct
|
6
|
+
# Reference to the document sent
|
7
|
+
attribute :transmissionReference, Types::Strict::String
|
8
|
+
# Date document was delivered
|
9
|
+
attribute :created, Types::JSON::DateTime.optional.default(nil)
|
10
|
+
# Date of last modification
|
11
|
+
attribute :modified, Types::JSON::DateTime.optional.default(nil)
|
12
|
+
# Current status of main delivery.
|
13
|
+
# The status is a summary of all subdeliveries.
|
14
|
+
attribute :status, Types::Strict::String.optional.default("".freeze)
|
15
|
+
end
|
16
|
+
|
17
|
+
class UploadControlData < Dry::Struct
|
18
|
+
# Transmission mode. The “Mode” control parameter must be identical to
|
19
|
+
# the “Mode” attribute in the XML document. Otherwise, an error will be generated.
|
20
|
+
attribute :mode, Types::Strict::String.optional.default(nil)
|
21
|
+
# Dispatch to organisation (no data warehouse).
|
22
|
+
# Overrides the “to” attribute (GLN recipient) in the XML document.
|
23
|
+
#
|
24
|
+
# In the case of “2000000000008” or “0000000000000”, the following applies
|
25
|
+
# • A TP invoice is not delivered to an organisation
|
26
|
+
# • A TG invoice is not delivered to an organisation, but to the TG warehouse.
|
27
|
+
#
|
28
|
+
# An error will be generated with all other differences as compared with
|
29
|
+
# the “to” attribute in XML.
|
30
|
+
attribute :toOrganization, Types::Strict::String.optional.default(nil)
|
31
|
+
# Dispatch to data warehouse (TrustCenter).
|
32
|
+
# Overrides the configured data warehouse in the MediData client.
|
33
|
+
# This must be a TrustCenter that the sender knows. See participant directory.
|
34
|
+
# No dispatch to a data warehouse will be overridden with
|
35
|
+
# value “2000000000008” or “0000000000000”.
|
36
|
+
attribute :toTrustCenter, Types::Strict::String.optional.default(nil)
|
37
|
+
# Dispatch to patients or to their legal representatives (guarantor, debtor).
|
38
|
+
#
|
39
|
+
# If the value is set to “true”, the dispatch will be triggered; if the
|
40
|
+
# value is set to “false”, dispatch will not take place.
|
41
|
+
# In the case of a Tiers Payant (TP) doc- ument, this value overrides the
|
42
|
+
# TP patient copy to the guarantors.
|
43
|
+
# In the case of a Tiers Garant (TG) doc- ument, this value overrides the TG
|
44
|
+
# patient invoice and the reimbursement receipt.
|
45
|
+
attribute :toPatient, Types::Strict::Bool.default(false)
|
46
|
+
# Defines the print language of the document (print layout).
|
47
|
+
# If this parameter is not set, it is read from the document or configuration.
|
48
|
+
attribute :printLanguage, Types::Strict::String.optional.default(nil)
|
49
|
+
# Type of postal dispatch.
|
50
|
+
# If this parameter is not set, it is read from the document or configuration.
|
51
|
+
attribute :postalDelivery, Types::Strict::String.optional.default(nil)
|
52
|
+
# Forces postal delivery to the patient.
|
53
|
+
# “true” forces printing with postal delivery
|
54
|
+
# (if multiple types of delivery are possible, such as electronic and post- al).
|
55
|
+
# “False” means “no forcing”.
|
56
|
+
# Application: e.g. a reminder should be sent to the guarantor/debtor by post.
|
57
|
+
attribute :forcePostalToPatient, Types::Strict::Bool.default(false)
|
58
|
+
# Contains a document designation unique to the participant.
|
59
|
+
# The reference data must consist solely of alphanumeric characters
|
60
|
+
# (incl. special characters) and be between min. 1 and max. 100 characters long.
|
61
|
+
attribute :documentReference, Types::Strict::String.optional.default(nil)
|
62
|
+
# Contains a business case number. This number is for all sent and received
|
63
|
+
# documents that belong to the same business case in technical terms.
|
64
|
+
#
|
65
|
+
# In this way, all documents belonging to the same business case can be
|
66
|
+
# clearly summarised. If this is missing, it is generated from the document
|
67
|
+
# content by the MediData client according to the description in the annex.
|
68
|
+
#
|
69
|
+
# The business number case must consist solely of alphanumeric characters
|
70
|
+
# (incl. special characters) and be between min.
|
71
|
+
# 1 and max. 100 characters long.
|
72
|
+
attribute :correlationReference, Types::Strict::String.optional.default(nil)
|
73
|
+
|
74
|
+
def to_json
|
75
|
+
s = {}
|
76
|
+
s[:mode] = mode if mode
|
77
|
+
s[:toOrganization] = toOrganization if toOrganization
|
78
|
+
s[:toTrustCenter] = toTrustCenter if toTrustCenter
|
79
|
+
s[:toPatient] = toPatient if toPatient
|
80
|
+
s[:printLanguage] = printLanguage if printLanguage
|
81
|
+
s[:postalDelivery] = postalDelivery if postalDelivery
|
82
|
+
s[:forcePostalToPatient] = forcePostalToPatient if forcePostalToPatient
|
83
|
+
s[:documentReference] = documentReference if documentReference
|
84
|
+
s[:correlationReference] = correlationReference if correlationReference
|
85
|
+
JSON.generate(s)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: medidata
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Denteo AG
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-07-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-types
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.5.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.5.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-struct
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.4.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.4.0
|
41
|
+
description: Ruby client interacting with Medidata REST API
|
42
|
+
email: simon@denteo.ch
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/medidata.rb
|
48
|
+
- lib/medidata/api.rb
|
49
|
+
- lib/medidata/api/client.rb
|
50
|
+
- lib/medidata/api/download.rb
|
51
|
+
- lib/medidata/api/error.rb
|
52
|
+
- lib/medidata/api/http.rb
|
53
|
+
- lib/medidata/api/notification.rb
|
54
|
+
- lib/medidata/api/participant.rb
|
55
|
+
- lib/medidata/api/type.rb
|
56
|
+
- lib/medidata/api/upload.rb
|
57
|
+
homepage: https://rubygems.org/gems/medidata
|
58
|
+
licenses:
|
59
|
+
- MIT
|
60
|
+
metadata: {}
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubygems_version: 3.1.2
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: Ruby client interacting with Medidata REST API
|
80
|
+
test_files: []
|