lifen-fhir 0.1.0

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +4 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +74 -0
  8. data/License.txt +22 -0
  9. data/README.md +131 -0
  10. data/lib/lifen.rb +29 -0
  11. data/lib/lifen/app_authenticated_client.rb +37 -0
  12. data/lib/lifen/attachment.rb +26 -0
  13. data/lib/lifen/base.rb +7 -0
  14. data/lib/lifen/binary.rb +31 -0
  15. data/lib/lifen/category.rb +26 -0
  16. data/lib/lifen/channel.rb +31 -0
  17. data/lib/lifen/client.rb +107 -0
  18. data/lib/lifen/communication_request.rb +67 -0
  19. data/lib/lifen/configuration.rb +22 -0
  20. data/lib/lifen/content_string.rb +15 -0
  21. data/lib/lifen/error.rb +27 -0
  22. data/lib/lifen/medium.rb +17 -0
  23. data/lib/lifen/patient.rb +27 -0
  24. data/lib/lifen/practitioner.rb +97 -0
  25. data/lib/lifen/user_authenticated_client.rb +39 -0
  26. data/lib/lifen/version.rb +3 -0
  27. data/lifen-fhir.gemspec +30 -0
  28. data/spec/binary_spec.rb +29 -0
  29. data/spec/cassettes/binary/download/invalid.yml +65 -0
  30. data/spec/cassettes/binary/download/valid.yml +1320 -0
  31. data/spec/cassettes/communication_request/send/invalid_medium.yml +69 -0
  32. data/spec/cassettes/communication_request/send/valid_attributes.yml +75 -0
  33. data/spec/cassettes/communication_request/send/valid_attributes_binary.yml +74 -0
  34. data/spec/cassettes/practitionner/create_channel/address/old_valid_attributes.yml +71 -0
  35. data/spec/cassettes/practitionner/create_channel/address/valid_attributes.yml +71 -0
  36. data/spec/cassettes/practitionner/create_channel/telecom/valid_attributes.yml +70 -0
  37. data/spec/cassettes/practitionner/find_by_rpps/existing_rpps.yml +125 -0
  38. data/spec/cassettes/practitionner/find_by_rpps/missing_line_attribute.yml +122 -0
  39. data/spec/cassettes/practitionner/find_by_rpps/wrong_rpps.yml +68 -0
  40. data/spec/category_spec.rb +21 -0
  41. data/spec/communication_request_spec.rb +64 -0
  42. data/spec/practitionner_spec.rb +76 -0
  43. data/spec/spec_helper.rb +24 -0
  44. data/spec/support/master_plan.pdf +0 -0
  45. metadata +230 -0
@@ -0,0 +1,107 @@
1
+ module Lifen
2
+ class Client
3
+
4
+ def request(mode, url, params = {})
5
+ before_request
6
+
7
+ response = faraday_client.send(mode) do |req|
8
+ req.url url
9
+
10
+ req.headers['Authorization'] = "Bearer #{bearer}"
11
+ req.headers['Accept'] = use_and_remove_accept(params)
12
+
13
+ if mode == :post
14
+ req.headers['Content-Type'] = "application/json"
15
+ end
16
+
17
+ req.body = JSON.generate(params)
18
+ end
19
+
20
+ handle_errors(response, params)
21
+
22
+ handle_response(response)
23
+ end
24
+
25
+ def post(url, params = {})
26
+ request(:post, url, params)
27
+ end
28
+
29
+ def put(url, params = {})
30
+ request(:put, url, params)
31
+ end
32
+
33
+ def get(url, params = {})
34
+ request(:get, url, params)
35
+ end
36
+
37
+ private
38
+
39
+ def handle_errors(response, params)
40
+ if response.status == 500
41
+
42
+ json = JSON.parse response.body
43
+
44
+ trace_id = json.fetch("X-B3-TraceId", "unknown")
45
+
46
+ raise Error, "Error 500, Internal server error (trace ID: #{trace_id}), #{response_error(response, params)}"
47
+ end
48
+ end
49
+
50
+ def handle_response(response)
51
+ if response.headers['Content-Type'].match "json"
52
+ JSON.parse response.body
53
+ else
54
+ response.body
55
+ end
56
+ end
57
+
58
+ def faraday_client
59
+ @faraday_client ||= Faraday.new(faraday_options) do |faraday|
60
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
61
+ end
62
+ end
63
+
64
+ def faraday_options
65
+ options = {url: site}
66
+
67
+ options[:proxy] = proxy_url if !proxy_url.nil?
68
+
69
+ options
70
+ end
71
+
72
+ def site
73
+ Lifen.configuration.site
74
+ end
75
+
76
+ def proxy_url
77
+ Lifen.configuration.proxy_url
78
+ end
79
+
80
+ def before_request
81
+ end
82
+
83
+ def response_error(response, params)
84
+ params[:payload] = "filtered" if params.is_a?(Hash) and params.has_key? :payload
85
+
86
+ "#{response.env.method.upcase} '#{response.env.url}' with params '#{params.inspect}' and bearer '#{trucanted_bearer}'"
87
+ end
88
+
89
+ def trucanted_bearer
90
+ if m = /^(.{24})(.*)$/.match(bearer)
91
+ "#{m[1]}#{"*" * m[2].length}"
92
+ else
93
+ bearer
94
+ end
95
+ end
96
+
97
+ def bearer
98
+ raise "A bearer method must be defined"
99
+ end
100
+
101
+ def use_and_remove_accept(params)
102
+ return params.delete :accept if params.is_a?(Hash) and params.has_key? :accept
103
+
104
+ "application/json"
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,67 @@
1
+ module Lifen
2
+ class CommunicationRequest < Base
3
+
4
+ attribute :uuid, String
5
+ attribute :number_communications, Integer
6
+
7
+ attribute :sender, Lifen::Practitioner
8
+ attribute :recipients, [Lifen::Practitioner]
9
+
10
+ attribute :category, Lifen::Category, default: Lifen::Category.new
11
+ attribute :medium, [Lifen::Medium]
12
+ attribute :patient, Lifen::Patient
13
+ attribute :attachment, Lifen::Attachment
14
+ attribute :binary, Lifen::Binary
15
+ attribute :content_string, Lifen::ContentString
16
+
17
+ def send
18
+ json = application_client.post("fhir/CommunicationRequest", fhir_payload)
19
+
20
+ self.uuid = json["id"]
21
+ self.number_communications = Array(json["issue"]).length
22
+
23
+ self
24
+ end
25
+
26
+ private
27
+
28
+ def fhir_payload
29
+
30
+ payload = {
31
+ resourceType: "CommunicationRequest",
32
+ sender: [ sender.fhir_payload ],
33
+ recipient: recipients.map(&:fhir_payload),
34
+ contained: [],
35
+ medium: medium.map(&:fhir_payload) ,
36
+ category: [ category.fhir_payload],
37
+ payload: [ document_content ]
38
+ }
39
+
40
+ if patient
41
+ payload[:contained] << patient.fhir_payload
42
+ payload[:subject] = [
43
+ {reference: "patient"}
44
+ ]
45
+ end
46
+
47
+ if content_string
48
+ payload[:payload] << content_string.fhir_payload
49
+ end
50
+
51
+ payload
52
+ end
53
+
54
+ def document_content
55
+ if !attachment.nil?
56
+ attachment.fhir_payload
57
+ else
58
+ binary.fhir_payload
59
+ end
60
+ end
61
+
62
+ def application_client
63
+ @application_client ||= AppAuthenticatedClient.new
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ module Lifen
2
+ class Configuration
3
+
4
+ attr_accessor :site, :application_access_token, :proxy_url
5
+
6
+ def site=(url)
7
+ if !/(.*)\/$/.match(url)
8
+ raise Lifen::Error, "Invalid 'site' provided in configuration: '#{url}', a trailing slash is missing"
9
+ end
10
+
11
+ @site = url
12
+ end
13
+ end
14
+
15
+ def self.configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def self.configure
20
+ yield(configuration) if block_given?
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Lifen
2
+
3
+ class ContentString < Base
4
+
5
+ attribute :text, String
6
+
7
+ def fhir_payload
8
+ {
9
+ contentString: text
10
+
11
+ }
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module Lifen
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ class UnauthorizedError < Error
7
+ end
8
+
9
+ class InvalidCredentialsError < Error
10
+ end
11
+
12
+ class InvalidParamsError < Error
13
+ end
14
+
15
+ class InvalidSecretTokenError < Error
16
+ end
17
+
18
+ class InvalidTokenError < Error
19
+ end
20
+
21
+ class UserAlreadyExistingError < Error
22
+ end
23
+
24
+ class CantRefreshTokenError < Error
25
+ end
26
+
27
+ end
@@ -0,0 +1,17 @@
1
+
2
+ module Lifen
3
+
4
+ class Medium < Base
5
+
6
+ attribute :uuid, String
7
+
8
+ def fhir_payload
9
+ {
10
+ coding:[{
11
+ id: uuid
12
+ }]
13
+ }
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ module Lifen
2
+ class Patient < Base
3
+
4
+ attribute :first_name, String
5
+ attribute :last_name, String
6
+ attribute :birthdate, Date
7
+
8
+ def fhir_payload
9
+ {
10
+ id: "patient",
11
+ resourceType: "Patient",
12
+ name: [
13
+ {
14
+ family: [
15
+ last_name
16
+ ],
17
+ given: [
18
+ first_name
19
+ ]
20
+ }
21
+ ],
22
+ birthDate: birthdate.to_s
23
+ }
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,97 @@
1
+ module Lifen
2
+ class Practitioner < Base
3
+
4
+ attribute :channels, [Lifen::Channel]
5
+
6
+ attribute :uuid, String
7
+ attribute :last_name, String
8
+ attribute :first_name, String
9
+ attribute :rpps, String
10
+
11
+ def fhir_payload
12
+ { reference: "Practitioner/#{uuid}" }
13
+ end
14
+
15
+ def self.find_by_rpps(rpps)
16
+ json = application_client.get("fhir/Practitioner/?identifier=#{rpps}")
17
+
18
+ raise "Practitioner not found" if Array(json["entry"]).size != 1
19
+
20
+ user_json = Array(json["entry"]).first.fetch("resource") { {} }
21
+
22
+ user_json[:uuid] = user_json["id"]
23
+
24
+ user = new(user_json)
25
+
26
+ Array(user_json["telecom"]).each do |telecom_json|
27
+ user.channels << Lifen::Channel.from_json(telecom_json, "telecom")
28
+ end
29
+
30
+ Array(user_json["address"]).each do |address_json|
31
+ user.channels << Lifen::Channel.from_json(address_json, "address")
32
+ end
33
+
34
+ user
35
+ end
36
+
37
+ def create_address(params)
38
+ filtered_params = {"resourceType" => "Practitioner"}
39
+
40
+ address = {
41
+ "line": Array(params[:lines]),
42
+ "city": params[:city],
43
+ "postalCode": params[:postal_code],
44
+ "country": params[:country]
45
+ }
46
+
47
+ filtered_params[params[:type]] = address
48
+
49
+ json = application_client.post("fhir/Practitioner/#{uuid}/$add-address", filtered_params)
50
+
51
+ channel = Channel.new(uuid: json["issue"][0]["id"], type: params[:type], value: "#{Array(params[:lines]).join(", ")}, #{params[:postal_code]} #{params[:city]}")
52
+
53
+ self.channels << channel
54
+
55
+ channel
56
+ end
57
+
58
+ def self.from_json(json)
59
+ reference = json["reference"]
60
+
61
+ uuid = reference.gsub("Practitioner/", "")
62
+
63
+ new(uuid: uuid)
64
+ end
65
+
66
+ def create_telecom(params)
67
+ filtered_params = {"resourceType" => "Practitioner"}
68
+
69
+ telecom = {
70
+ "system": params[:system],
71
+ "value": params[:value]
72
+ }
73
+
74
+ filtered_params[params[:type]] = telecom
75
+
76
+ json = application_client.post("fhir/Practitioner/#{uuid}/$add-telecom", filtered_params)
77
+
78
+ channel = Channel.new(uuid: json["issue"][0]["id"], type: params[:type], value: params[:value])
79
+
80
+ self.channels << channel
81
+
82
+ channel
83
+
84
+ end
85
+
86
+ private
87
+
88
+ def application_client
89
+ @application_client ||= AppAuthenticatedClient.new
90
+ end
91
+
92
+ def self.application_client
93
+ @application_client ||= AppAuthenticatedClient.new
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,39 @@
1
+ module Lifen
2
+ class UserAuthenticatedClient < Client
3
+
4
+ def initialize(token)
5
+ @token = token
6
+ end
7
+
8
+ attr_reader :token
9
+
10
+ private
11
+
12
+ def handle_errors(response, params)
13
+ super(response, params)
14
+
15
+ case response.status
16
+ when 400
17
+ raise InvalidParamsError, "Error 400, Invalid params, #{response_error(response, params)}"
18
+ when 401
19
+ raise UnauthorizedError, "Error 401, Token is not valid, #{response_error(response, params)}"
20
+ when 403
21
+ raise Error, "Error 403, Action is forbidden, #{response_error(response, params)}"
22
+ end
23
+
24
+ end
25
+
26
+ def response_error(response, params)
27
+ "User Client, #{super(response, params)}"
28
+ end
29
+
30
+ def bearer
31
+ token.value
32
+ end
33
+
34
+ def before_request
35
+ token.refresh_once_if_needed
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module Lifen
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'lifen/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "lifen-fhir"
7
+ spec.version = Lifen::VERSION
8
+ spec.authors = ["Leonard Sellam"]
9
+ spec.email = ["leonard@lifen.fr"]
10
+ spec.homepage = "https://github.com/honestica/lifen-fhir"
11
+ spec.description = %q{Lifen FHIR API Ruby client}
12
+ spec.summary = %q{Lifen FHIR API Ruby client}
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", '~> 1.12'
21
+ spec.add_development_dependency "rake", '~> 10.5'
22
+ spec.add_development_dependency "rspec", '~> 3.5'
23
+ spec.add_development_dependency "vcr", '~> 3.0'
24
+ spec.add_development_dependency "webmock", '~> 1.24'
25
+ spec.add_development_dependency "awesome_print"
26
+
27
+ spec.add_runtime_dependency "virtus", '>= 1.0'
28
+ spec.add_runtime_dependency "inflecto"
29
+ spec.add_runtime_dependency "faraday", '>= 0.9'
30
+ end