medidata 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/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: []
|