Skip to content
This repository was archived by the owner on Jan 29, 2026. It is now read-only.

Commit 17b0392

Browse files
authored
Merge pull request #28 from philips-software/feature/cdr
CDR tenant onboarding
2 parents 2df5992 + 3e660c8 commit 17b0392

File tree

12 files changed

+1558
-5
lines changed

12 files changed

+1558
-5
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ The current implement covers only a subset of HSDP APIs. Basically we implement
1313

1414
- [x] Cartel c.q. Container Host management ([examples](cartel/README.md))
1515
- [x] IronIO tasks, codes and schedules management ([examples](iron/README.md))
16-
- [x] HSDP PKI services
17-
- [x] HSDP IAM/IDM management
16+
- [x] PKI services
17+
- [x] IAM/IDM management
1818
- [x] Groups
1919
- [x] Organizations
2020
- [x] Permissions
@@ -29,6 +29,16 @@ The current implement covers only a subset of HSDP APIs. Basically we implement
2929
- [x] Password Policies
3030
- [x] Logging ([examples](logging/README.md))
3131
- [ ] Auditing
32+
- [x] Clinical Data Repository (CDR)
33+
- [x] Tenant Onboarding
34+
- [ ] Subscription management
35+
- [x] FHIR Patch
36+
- [x] FHIR Post
37+
- [ ] FHIR Put
38+
- [ ] FHIR Delete
39+
- [x] Telemetry Data Repository (TDR)
40+
- [x] Contract management
41+
- [x] Data Item management
3242
- [x] S3 Credentials Policy management
3343
- [x] Hosted Application Streaming (HAS) management
3444
- [x] Configuration

cdr/client.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// Package pki provides support for HSDP CDR service
2+
//
3+
// We only intent to support the CDR FHIR STU3 and newer with this library.
4+
package cdr
5+
6+
import (
7+
"bytes"
8+
"encoding/json"
9+
"fmt"
10+
"io"
11+
"io/ioutil"
12+
"net/http"
13+
"net/http/httputil"
14+
"net/url"
15+
"os"
16+
"strings"
17+
18+
"github.com/google/fhir/go/jsonformat"
19+
20+
"github.com/philips-software/go-hsdp-api/iam"
21+
)
22+
23+
const (
24+
libraryVersion = "0.27.0"
25+
userAgent = "go-hsdp-api/cdr/" + libraryVersion
26+
APIVersion = "1"
27+
)
28+
29+
// OptionFunc is the function signature function for options
30+
type OptionFunc func(*http.Request) error
31+
32+
// Config contains the configuration of a client
33+
type Config struct {
34+
Region string
35+
Environment string
36+
RootOrgID string
37+
CDRURL string
38+
FHIRStore string
39+
TimeZone string
40+
DebugLog string
41+
}
42+
43+
// A Client manages communication with HSDP CDR API
44+
type Client struct {
45+
// HTTP client used to communicate with IAM API
46+
iamClient *iam.Client
47+
48+
config *Config
49+
50+
fhirStoreURL *url.URL
51+
52+
// User agent used when communicating with the HSDP IAM API.
53+
UserAgent string
54+
55+
TenantSTU3 *TenantSTU3Service
56+
OperationsSTU3 *OperationsSTU3Service
57+
58+
debugFile *os.File
59+
}
60+
61+
// NewClient returns a new HSDP CDR API client. Configured console and IAM clients
62+
// must be provided as the underlying API requires tokens from respective services
63+
func NewClient(iamClient *iam.Client, config *Config) (*Client, error) {
64+
return newClient(iamClient, config)
65+
}
66+
67+
func newClient(iamClient *iam.Client, config *Config) (*Client, error) {
68+
c := &Client{iamClient: iamClient, config: config, UserAgent: userAgent}
69+
fhirStore := config.FHIRStore
70+
if fhirStore == "" {
71+
fhirStore = config.CDRURL + "/store/fhir/"
72+
}
73+
if err := c.SetFHIRStoreURL(fhirStore); err != nil {
74+
return nil, err
75+
}
76+
if config.DebugLog != "" {
77+
var err error
78+
c.debugFile, err = os.OpenFile(config.DebugLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
79+
if err != nil {
80+
c.debugFile = nil
81+
}
82+
}
83+
ma, err := jsonformat.NewMarshaller(false, "", "", jsonformat.STU3)
84+
if err != nil {
85+
return nil, err
86+
}
87+
um, err := jsonformat.NewUnmarshaller(config.TimeZone, jsonformat.STU3)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
c.TenantSTU3 = &TenantSTU3Service{timeZone: config.TimeZone, client: c, ma: ma, um: um}
93+
c.OperationsSTU3 = &OperationsSTU3Service{timeZone: config.TimeZone, client: c, ma: ma, um: um}
94+
95+
return c, nil
96+
}
97+
98+
// Close releases allocated resources of clients
99+
func (c *Client) Close() {
100+
if c.debugFile != nil {
101+
_ = c.debugFile.Close()
102+
c.debugFile = nil
103+
}
104+
}
105+
106+
// GetFHIRStoreURL returns the base FHIR Store URL as configured
107+
func (c *Client) GetFHirStoreURL() string {
108+
if c.fhirStoreURL == nil {
109+
return ""
110+
}
111+
return c.fhirStoreURL.String()
112+
}
113+
114+
// SetFHIRStoreURL sets the base URL for API requests to a custom endpoint. urlStr
115+
// should always be specified with a trailing slash.
116+
func (c *Client) SetFHIRStoreURL(urlStr string) error {
117+
if urlStr == "" {
118+
return ErrCDRURLCannotBeEmpty
119+
}
120+
// Make sure the given URL end with a slash
121+
if !strings.HasSuffix(urlStr, "/") {
122+
urlStr += "/"
123+
}
124+
125+
var err error
126+
c.fhirStoreURL, err = url.Parse(urlStr)
127+
return err
128+
}
129+
130+
// NewCDRRequest creates an new CDR Service API request. A relative URL path can be provided in
131+
// urlStr, in which case it is resolved relative to the base URL of the Client.
132+
// Relative URL paths should always be specified without a preceding slash. If
133+
// specified, the value pointed to by body is JSON encoded and included as the
134+
// request body.
135+
func (c *Client) NewCDRRequest(method, path string, bodyBytes []byte, options []OptionFunc) (*http.Request, error) {
136+
u := *c.fhirStoreURL
137+
// Set the encoded opaque data
138+
u.Opaque = c.fhirStoreURL.Path + c.config.RootOrgID + "/" + path
139+
140+
req := &http.Request{
141+
Method: method,
142+
URL: &u,
143+
Proto: "HTTP/1.1",
144+
ProtoMajor: 1,
145+
ProtoMinor: 1,
146+
Header: make(http.Header),
147+
Host: u.Host,
148+
}
149+
150+
for _, fn := range options {
151+
if fn == nil {
152+
continue
153+
}
154+
155+
if err := fn(req); err != nil {
156+
return nil, err
157+
}
158+
}
159+
160+
if method == "POST" || method == "PUT" || method == "PATCH" {
161+
bodyReader := bytes.NewReader(bodyBytes)
162+
163+
u.RawQuery = ""
164+
req.Body = ioutil.NopCloser(bodyReader)
165+
req.ContentLength = int64(bodyReader.Len())
166+
}
167+
168+
req.Header.Set("Accept", "*/*")
169+
req.Header.Set("Authorization", "Bearer "+c.iamClient.Token())
170+
req.Header.Set("API-Version", APIVersion)
171+
172+
if c.UserAgent != "" {
173+
req.Header.Set("User-Agent", c.UserAgent)
174+
}
175+
return req, nil
176+
}
177+
178+
// Response is a HSDP IAM API response. This wraps the standard http.Response
179+
// returned from HSDP IAM and provides convenient access to things like errors
180+
type Response struct {
181+
*http.Response
182+
}
183+
184+
// newResponse creates a new Response for the provided http.Response.
185+
func newResponse(r *http.Response) *Response {
186+
response := &Response{Response: r}
187+
return response
188+
}
189+
190+
// Do executes a http request. If v implements the io.Writer
191+
// interface, the raw response body will be written to v, without attempting to
192+
// first decode it.
193+
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
194+
if c.debugFile != nil {
195+
dumped, _ := httputil.DumpRequest(req, true)
196+
out := fmt.Sprintf("[go-hsdp-api] --- Request start ---\n%s\n[go-hsdp-api] Request end ---\n", string(dumped))
197+
_, _ = c.debugFile.WriteString(out)
198+
}
199+
resp, err := c.iamClient.HttpClient().Do(req)
200+
if c.debugFile != nil && resp != nil {
201+
dumped, _ := httputil.DumpResponse(resp, true)
202+
out := fmt.Sprintf("[go-hsdp-api] --- Response start ---\n%s\n[go-hsdp-api] --- Response end ---\n", string(dumped))
203+
_, _ = c.debugFile.WriteString(out)
204+
}
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
response := newResponse(resp)
210+
211+
err = CheckResponse(resp)
212+
if err != nil {
213+
// even though there was an error, we still return the response
214+
// in case the caller wants to inspect it further
215+
return response, err
216+
}
217+
218+
if v != nil {
219+
defer resp.Body.Close() // Only close if we plan to read it
220+
if w, ok := v.(io.Writer); ok {
221+
_, err = io.Copy(w, resp.Body)
222+
} else {
223+
err = json.NewDecoder(resp.Body).Decode(v)
224+
}
225+
}
226+
227+
return response, err
228+
}
229+
230+
// CheckResponse checks the API response for errors, and returns them if present.
231+
func CheckResponse(r *http.Response) error {
232+
switch r.StatusCode {
233+
case 200, 201, 202, 204, 304:
234+
return nil
235+
}
236+
return ErrNonHttp20xResponse
237+
}

0 commit comments

Comments
 (0)