Skip to content

Commit 7c0868f

Browse files
authored
Merge pull request #156 from patientsknowbest/feature/PHR-16180-structure-definiton-override
Structure definiton override PHR-16180
2 parents 28e1211 + 6e40bd9 commit 7c0868f

11 files changed

+6768
-21
lines changed

aidbox/api_client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ func (apiClient *ApiClient) send(ctx context.Context, requestBody interface{}, r
8888
// Deletes in general return the resource you deleted in the response body, but sometimes not (e.g. SearchParameter)
8989
if httpMethod == http.MethodDelete && len(body) == 0 {
9090
return nil
91-
} else {
92-
return json.Unmarshal(body, responseT)
9391
}
92+
93+
return json.Unmarshal(body, responseT)
9494
}
9595

9696
func (apiClient *ApiClient) get(ctx context.Context, relativePath string, responseT interface{}) error {

aidbox/api_client_test.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package aidbox
33
import (
44
"context"
55
"fmt"
6-
"github.com/stretchr/testify/assert"
76
"net/http"
87
"net/http/httptest"
8+
"os"
99
"testing"
10+
11+
"github.com/stretchr/testify/assert"
1012
)
1113

1214
type TestResponse struct {
@@ -58,10 +60,28 @@ Service Unavailable
5860
client := NewApiClient(server.URL, "foo", "bar")
5961
err := client.post(context.TODO(), "", "/endpoint", "")
6062

63+
sensitiveDetails := ""
64+
if os.Getenv("TF_ACC") == "1" {
65+
sensitiveDetails = fmt.Sprintf(`
66+
===== REQUEST HEADERS =====
67+
{
68+
"Authorization": [
69+
"Basic Zm9vOmJhcg=="
70+
],
71+
"Content-Type": [
72+
"application/json"
73+
]
74+
}
75+
76+
===== REQUEST BODY =====
77+
""
78+
`)
79+
}
80+
6181
expectedError := fmt.Sprintf(`unexpected status code (422) received: 422 Unprocessable Entity
6282
6383
===== POST %s/endpoint =====
64-
84+
%s
6585
===== RESPONSE BODY =====
6686
{
6787
"name": "Ankh-Morpork City Watch",
@@ -75,7 +95,7 @@ Service Unavailable
7595
"Reg Shoe"
7696
]
7797
}
78-
`, server.URL)
98+
`, server.URL, sensitiveDetails)
7999

80100
assert.Equal(t, expectedError, err.Error())
81101
})

aidbox/fhir.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package aidbox
2+
3+
import "encoding/json"
4+
5+
type Bundle struct {
6+
Entry []BundleEntry `json:"entry"`
7+
}
8+
9+
type BundleEntry struct {
10+
Resource json.RawMessage `json:"resource"`
11+
}

aidbox/structure_definition.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@ package aidbox
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
7+
"net/url"
68
)
79

810
// StructureDefinition Represents a subset of the FHIR R4 spec "StructureDefinition", this is not a full support
9-
// for the resource. Used only to allow testing and temporarily support defining profiles
11+
// for the resource.
12+
// This supports two ways of interacting with the StructureDefinition
13+
// - the "regular" methods as seen in other resource implementations only parse and write a subset of the resource,
14+
// this is for setting up custom profiles in the "FHIR-way"
15+
// - the "byUrl" methods, these parse and write the entirety of the resource but provide no type checking, and used
16+
// for overriding core FHIR spec StructureDefinitions, which is an aidbox specific feature
1017
type StructureDefinition struct {
1118
ResourceBase
1219
ResourceType string `json:"resourceType,omitempty"`
@@ -48,3 +55,29 @@ func (apiClient *ApiClient) UpdateStructureDefinition(ctx context.Context, q *St
4855
func (apiClient *ApiClient) DeleteStructureDefinition(ctx context.Context, id string) error {
4956
return apiClient.deleteResource(ctx, id, &StructureDefinition{})
5057
}
58+
59+
func (apiClient *ApiClient) GetStructureDefinitionByUrl(ctx context.Context, canonicalUrl string) (*map[string]interface{}, error) {
60+
response := &Bundle{}
61+
err := apiClient.get(ctx, fmt.Sprintf("/fhir/StructureDefinition?url=%s", url.QueryEscape(canonicalUrl)), response)
62+
if err != nil {
63+
return nil, err
64+
}
65+
if len(response.Entry) == 0 {
66+
return nil, fmt.Errorf("StructureDefinition with canonical url '%s' does not exist", canonicalUrl)
67+
}
68+
if len(response.Entry) > 1 {
69+
return nil, fmt.Errorf("found %d StructureDefinition entries for canonical url '%s', expected 1", len(response.Entry), canonicalUrl)
70+
}
71+
resource := response.Entry[0].Resource
72+
var structureDefinition = map[string]interface{}{}
73+
err = json.Unmarshal(resource, &structureDefinition)
74+
if err != nil {
75+
return nil, err
76+
}
77+
return &structureDefinition, nil
78+
}
79+
80+
func (apiClient *ApiClient) UpdateStructureDefinitionByUrl(ctx context.Context, sd *map[string]interface{}, canonicalUrl string) (*map[string]interface{}, error) {
81+
response := &map[string]interface{}{}
82+
return response, apiClient.put(ctx, sd, fmt.Sprintf("/fhir/StructureDefinition?url=%s", url.QueryEscape(canonicalUrl)), response)
83+
}

internal/provider/provider.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,20 @@ func New(apiClient *aidbox.ApiClient) func() *schema.Provider {
5252
"aidbox_user": dataSourceUser(),
5353
},
5454
ResourcesMap: map[string]*schema.Resource{
55-
"aidbox_token_introspector": resourceTokenIntrospector(),
56-
"aidbox_access_policy": resourceAccessPolicy(),
57-
"aidbox_client": resourceClient(),
58-
"aidbox_db_migration": resourceDbMigration(),
59-
"aidbox_search": resourceSearch(),
60-
"aidbox_search_parameter": resourceSearchParameter(),
61-
"aidbox_fhir_search_parameter": resourceSearchParameterV2(),
62-
"aidbox_aidbox_subscription_topic": resourceAidboxSubscriptionTopic(),
63-
"aidbox_aidbox_topic_destination": resourceAidboxTopicDestination(),
64-
"aidbox_identity_provider": resourceIdentityProvider(),
65-
"aidbox_structure_definition": resourceStructureDefinition(),
66-
"aidbox_sdc_config": resourceSDCConfig(),
67-
"aidbox_gcp_service_account": resourceGcpServiceAccount(),
55+
"aidbox_token_introspector": resourceTokenIntrospector(),
56+
"aidbox_access_policy": resourceAccessPolicy(),
57+
"aidbox_client": resourceClient(),
58+
"aidbox_db_migration": resourceDbMigration(),
59+
"aidbox_search": resourceSearch(),
60+
"aidbox_search_parameter": resourceSearchParameter(),
61+
"aidbox_fhir_search_parameter": resourceSearchParameterV2(),
62+
"aidbox_aidbox_subscription_topic": resourceAidboxSubscriptionTopic(),
63+
"aidbox_aidbox_topic_destination": resourceAidboxTopicDestination(),
64+
"aidbox_identity_provider": resourceIdentityProvider(),
65+
"aidbox_structure_definition": resourceStructureDefinition(),
66+
"aidbox_structure_definition_override": resourceStructureDefinitionOverride(),
67+
"aidbox_sdc_config": resourceSDCConfig(),
68+
"aidbox_gcp_service_account": resourceGcpServiceAccount(),
6869
},
6970
}
7071

internal/provider/resource_structure_definition.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111

1212
func resourceStructureDefinition() *schema.Resource {
1313
return &schema.Resource{
14-
Description: "FHIR R4 SearchParameter https://hl7.org/fhir/R4/searchparameter.html",
14+
Description: "FHIR R4 StructureDefinition https://hl7.org/fhir/R4/structuredefinition.html Provides " +
15+
"limited support to specify custom StructureDefinitions, that express customized rules extending the core " +
16+
"FHIR spec, and get evaluated only if the caller specifies the SD's url in the request's meta.profile",
1517
CreateContext: resourceStructureDefinitionCreate,
1618
ReadContext: resourceStructureDefinitionRead,
1719
UpdateContext: resourceStructureDefinitionUpdate,
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
"github.com/patientsknowbest/terraform-provider-aidbox/aidbox"
11+
)
12+
13+
func resourceStructureDefinitionOverride() *schema.Resource {
14+
// Note there's no import functionality here. Since the initial create has to read the original spec, it's impossible
15+
// to import that as that would be already overwritten. This would be probably possible if we asked the user
16+
// to store the core SD in an attribute, but that adds extra complexity and handling.
17+
return &schema.Resource{
18+
Description: "A specialization of StructureDefinition which allows you to override the default version of" +
19+
" StructureDefinitions that are specified inside the core FHIR IG used on the server. This means default " +
20+
"rules of resources can be changed without having the client specify a meta.profile in their request.",
21+
CreateContext: resourceStructureDefinitionOverrideCreate,
22+
ReadContext: resourceStructureDefinitionOverrideRead,
23+
UpdateContext: resourceStructureDefinitionOverrideUpdate,
24+
DeleteContext: resourceStructureDefinitionOverrideDelete,
25+
Schema: resourceFullSchema(resourceSchemaStructureDefinitionOverride()),
26+
}
27+
}
28+
29+
func resourceSchemaStructureDefinitionOverride() map[string]*schema.Schema {
30+
return map[string]*schema.Schema{
31+
"url": {
32+
Description: "Canonical URL that's unique to this StructureDefinition",
33+
Type: schema.TypeString,
34+
Required: true,
35+
// url is essentially the logical id, hence it's impossible to update it
36+
// if you change it, what you mean is: destroy the changed override, and add a new one under this url
37+
ForceNew: true,
38+
},
39+
"structure_definition_override": {
40+
Description: "A customized StructureDefinition, based on the original one from the core FHIR spec",
41+
Type: schema.TypeString,
42+
Required: true,
43+
DiffSuppressOnRefresh: true,
44+
DiffSuppressFunc: jsonDiffSuppressFunc,
45+
},
46+
"original_structure_definition": {
47+
Description: "Backup of the original StructureDefinition, which will be restored upon deleting the override",
48+
Type: schema.TypeString,
49+
Computed: true,
50+
Sensitive: true,
51+
},
52+
}
53+
}
54+
55+
func resourceStructureDefinitionOverrideCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
56+
apiClient := meta.(*aidbox.ApiClient)
57+
58+
// look up the original, FHIR spec StructureDefinition by the canonical URL
59+
// this is required to create a backup in tf state so we can restore it if we want to delete the
60+
// customized one - we must never delete the original as it would break FHIR functionality
61+
canonicalUrl := d.Get("url").(string)
62+
63+
originalSD, err := apiClient.GetStructureDefinitionByUrl(ctx, canonicalUrl)
64+
65+
if err != nil {
66+
return diag.FromErr(err)
67+
}
68+
69+
originalSDBytes, err := json.Marshal(originalSD)
70+
d.Set("original_structure_definition", string(originalSDBytes))
71+
72+
overrideSDString := d.Get("structure_definition_override").(string)
73+
74+
overrideSD := map[string]interface{}{}
75+
err = json.Unmarshal([]byte(overrideSDString), &overrideSD)
76+
if err != nil {
77+
return diag.FromErr(err)
78+
}
79+
80+
// now update the SD to our customized version
81+
updatedSD, err := apiClient.UpdateStructureDefinitionByUrl(ctx, &overrideSD, canonicalUrl)
82+
if err != nil {
83+
return diag.FromErr(err)
84+
}
85+
var updatedUrl = (*updatedSD)["url"].(string)
86+
if updatedUrl != canonicalUrl {
87+
return diag.FromErr(fmt.Errorf("canonical url of resource unexpectedly changed after update, %s was set on the resource but server responded with %s", canonicalUrl, updatedUrl))
88+
}
89+
d.Set("url", updatedUrl)
90+
// throw away the id we didn't know upfront, it just adds unnecessary complexity here when comparing states
91+
delete(*updatedSD, "id")
92+
// terraform mandates that we have an id though, so use the url for that
93+
d.SetId(updatedUrl)
94+
95+
updatedSDBytes, err := json.Marshal(updatedSD)
96+
updatedSDString := string(updatedSDBytes)
97+
if err != nil {
98+
return diag.FromErr(err)
99+
}
100+
d.Set("structure_definition_override", updatedSDString)
101+
102+
return nil
103+
}
104+
105+
func resourceStructureDefinitionOverrideRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
106+
apiClient := meta.(*aidbox.ApiClient)
107+
108+
canonicalUrl := d.Get("url").(string)
109+
overrideSD, err := apiClient.GetStructureDefinitionByUrl(ctx, canonicalUrl)
110+
if err != nil {
111+
if handleNotFoundError(err, d) {
112+
return nil
113+
}
114+
return diag.FromErr(err)
115+
}
116+
117+
var overrideSDUrl = (*overrideSD)["url"].(string)
118+
if overrideSDUrl != canonicalUrl {
119+
return diag.FromErr(fmt.Errorf("canonical url of resource unexpectedly changed during state refresh, %s was set on the resource but server responded with %s", canonicalUrl, overrideSDUrl))
120+
}
121+
d.Set("url", overrideSDUrl)
122+
// throw away the id we didn't know upfront, it just adds unnecessary complexity here when comparing states
123+
delete(*overrideSD, "id")
124+
// terraform mandates that we have an id though, so use the url for that
125+
d.SetId(overrideSDUrl)
126+
127+
overrideSDBytes, err := json.Marshal(overrideSD)
128+
overrideSDString := string(overrideSDBytes)
129+
if err != nil {
130+
return diag.FromErr(err)
131+
}
132+
d.Set("structure_definition_override", overrideSDString)
133+
134+
return nil
135+
}
136+
137+
func resourceStructureDefinitionOverrideUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
138+
apiClient := meta.(*aidbox.ApiClient)
139+
140+
canonicalUrl := d.Get("url").(string)
141+
overrideSDString := d.Get("structure_definition_override").(string)
142+
143+
overrideSD := map[string]interface{}{}
144+
err := json.Unmarshal([]byte(overrideSDString), &overrideSD)
145+
if err != nil {
146+
return diag.FromErr(err)
147+
}
148+
149+
// update the SD to our new customized version
150+
updatedSD, err := apiClient.UpdateStructureDefinitionByUrl(ctx, &overrideSD, canonicalUrl)
151+
if err != nil {
152+
return diag.FromErr(err)
153+
}
154+
// throw away the id we didn't know upfront, it just adds unnecessary complexity here when comparing states
155+
delete(*updatedSD, "id")
156+
157+
updatedSDBytes, err := json.Marshal(updatedSD)
158+
if err != nil {
159+
return diag.FromErr(err)
160+
}
161+
d.Set("structure_definition_override", string(updatedSDBytes))
162+
163+
return nil
164+
}
165+
166+
func resourceStructureDefinitionOverrideDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
167+
apiClient := meta.(*aidbox.ApiClient)
168+
169+
// no such thing as deleting from the core fhir spec, this just means we restore the original spec
170+
canonicalUrl := d.Get("url").(string)
171+
originalSDString := d.Get("original_structure_definition").(string)
172+
173+
originalSD := map[string]interface{}{}
174+
err := json.Unmarshal([]byte(originalSDString), &originalSD)
175+
if err != nil {
176+
return diag.FromErr(err)
177+
}
178+
179+
// restore the SD to the spec version
180+
if _, err := apiClient.UpdateStructureDefinitionByUrl(ctx, &originalSD, canonicalUrl); err != nil {
181+
return diag.FromErr(err)
182+
}
183+
184+
return nil
185+
}

0 commit comments

Comments
 (0)