Skip to content

Commit

Permalink
Extract extension contexts to keyword
Browse files Browse the repository at this point in the history
When processing an Extension definition, set the contexts directly on
the ExportableExtension object. When doing so, convert paths back to
FSH-style paths. When extracting caret value rules on an Extension
definition, do not extract any rules for contexts.

Add optimizer plugin to resolve URLs in contexts.
  • Loading branch information
mint-thompson committed Sep 28, 2023
1 parent f56e8fa commit 70a4e78
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 2 deletions.
6 changes: 6 additions & 0 deletions src/extractor/CaretValueRuleExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ export class CaretValueRuleExtractor {
parent = cloneDeep(sd);
}

// if this is an Extension, ignore context, since it was covered by a keyword
if (sd.derivation === 'constraint' && sd.type === 'Extension') {
delete sd.context;
delete parent.context;
}

// Remove properties that are covered by other extractors or keywords
RESOURCE_IGNORED_PROPERTIES['StructureDefinition'].forEach(prop => {
delete sd[prop];
Expand Down
59 changes: 59 additions & 0 deletions src/optimizer/plugins/ResolveContextURLsOptimizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { utils } from 'fsh-sushi';
import { OptimizerPlugin } from '../OptimizerPlugin';
import { optimizeURL } from '../utils';
import { Package } from '../../processor';
import { MasterFisher, ProcessingOptions } from '../../utils';
import { isUri } from 'valid-url';

const FISHER_TYPES = [
utils.Type.Resource,
utils.Type.Type,
utils.Type.Profile,
utils.Type.Extension,
utils.Type.Logical
];

export default {
name: 'resolve_context_urls',
description: 'Replace declared extension context URLs with their names or aliases',

optimize(pkg: Package, fisher: MasterFisher, options: ProcessingOptions = {}): void {
for (const extension of pkg.extensions) {
if (extension.contexts) {
extension.contexts.forEach(context => {
if (!context.isQuoted) {
// if the context is an extension, value is just a url without a #
// if the context is an element of a non-core resource, value is a url, #, and a path
if (context.value.indexOf('#') > -1) {
const [url, path] = context.value.split('#');
const newUrl = optimizeURL(
url,
pkg.aliases,
FISHER_TYPES,
fisher,
options.alias ?? true
);
if (newUrl !== url) {
let separator: string;
if (newUrl.startsWith('$')) {
separator = '#';
} else {
separator = '.';
}
context.value = `${newUrl}${separator}${path}`;
}
} else if (isUri(context.value)) {
context.value = optimizeURL(
context.value,
pkg.aliases,
FISHER_TYPES,
fisher,
options.alias ?? true
);
}
}
});
}
}
}
} as OptimizerPlugin;
2 changes: 2 additions & 0 deletions src/optimizer/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import RemoveGeneratedTextRulesOptimizer from './RemoveGeneratedTextRulesOptimiz
import RemoveImpliedZeroZeroCardRulesOptimize from './RemoveImpliedZeroZeroCardRulesOptimizer';
import RemovePublisherDerivedDateRulesOptimizer from './RemovePublisherDerivedDateRulesOptimizer';
import ResolveBindingRuleURLsOptimizer from './ResolveBindingRuleURLsOptimizer';
import ResolveContextURLsOptimizer from './ResolveContextURLsOptimizer';
import ResolveInstanceOfURLsOptimizer from './ResolveInstanceOfURLsOptimizer';
import ResolveOnlyRuleURLsOptimizer from './ResolveOnlyRuleURLsOptimizer';
import ResolveParentURLsOptimizer from './ResolveParentURLsOptimizer';
Expand Down Expand Up @@ -45,6 +46,7 @@ export {
RemoveImpliedZeroZeroCardRulesOptimize,
RemovePublisherDerivedDateRulesOptimizer,
ResolveBindingRuleURLsOptimizer,
ResolveContextURLsOptimizer,
ResolveInstanceOfURLsOptimizer,
ResolveOnlyRuleURLsOptimizer,
ResolveParentURLsOptimizer,
Expand Down
2 changes: 1 addition & 1 deletion src/optimizer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function optimizeURL(

/**
* Resolves a URL to a name, if possible; otherwise returns undefined. If the URL resolves to a name,
* but the name does not resolve back to the same URL, then return udnefined since the name clashes with
* but the name does not resolve back to the same URL, then return undefined since the name clashes with
* a more preferred name. This can happen if a project defines something with the same name as a FHIR
* definition.
* @param url - the url to resolve
Expand Down
48 changes: 47 additions & 1 deletion src/processor/StructureDefinitionProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import {
MappingExtractor
} from '../extractor';
import { ProcessableElementDefinition, switchQuantityRules, makeNameSushiSafe } from '.';
import { getAncestorSliceDefinition, logger } from '../utils';
import { getAncestorSliceDefinition, getPath, logger } from '../utils';
import { fshifyString } from '../exportable/common';
import { isUri } from 'valid-url';

export class StructureDefinitionProcessor {
static process(
Expand Down Expand Up @@ -98,6 +100,49 @@ export class StructureDefinitionProcessor {
if (input.baseDefinition) {
target.parent = input.baseDefinition;
}
if (target instanceof ExportableExtension && input.context) {
target.contexts = input.context.map(ctx => {
if (ctx.type === 'fhirpath') {
return {
isQuoted: true,
value: fshifyString(ctx.expression)
};
}
// element and extension contexts are a little trickier, since they may involve paths.
// we'll make a little ElementDefinition to help us out.
// if there's a #, or the whole value is a valid URL, wait until later to try to resolve the URL, since it may refer to
// another resource being processed.
// but either way, we can handle the fhirPath now.
if (ctx.expression.indexOf('#') > -1) {
const [url, fhirPath] = ctx.expression.split('#');
const fakeElement = new fhirtypes.ElementDefinition(fhirPath);
const fshPath = getPath(fakeElement);
// the fshPath from getPath removes the resource name, which is convenient here
return {
isQuoted: false,
value: `${url}#${fshPath}`
};
} else if (isUri(ctx.expression)) {
return {
isQuoted: false,
value: ctx.expression
};
} else {
const fakeElement = new fhirtypes.ElementDefinition(ctx.expression);
const fshPath = getPath(fakeElement);
// the fshPath from getPath removes the resource name, so add the resource name back to the start
// it will turn a resource name by itself into the path ".", which we don't need
let contextValue = ctx.expression.split('.')[0];
if (fshPath !== '.') {
contextValue += `.${fshPath}`;
}
return {
isQuoted: false,
value: contextValue
};
}
});
}
}

static extractRules(
Expand Down Expand Up @@ -230,6 +275,7 @@ export interface ProcessableStructureDefinition {
kind?: string;
derivation?: string;
mapping?: fhirtypes.StructureDefinitionMapping[];
context?: fhirtypes.StructureDefinitionContext[];
differential?: {
element: any[];
};
Expand Down
15 changes: 15 additions & 0 deletions test/extractor/CaretValueRuleExtractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('CaretValueRuleExtractor', () => {
let looseCS: any;
let looseBSSD: any;
let looseTPESD: any;
let looseExtSD: any;
let config: fshtypes.Configuration;
let defs: FHIRDefinitions;

Expand Down Expand Up @@ -48,6 +49,11 @@ describe('CaretValueRuleExtractor', () => {
)
.trim()
);
looseExtSD = JSON.parse(
fs
.readFileSync(path.join(__dirname, 'fixtures', 'extension-with-context.json'), 'utf-8')
.trim()
);
});

beforeEach(() => {
Expand All @@ -60,6 +66,15 @@ describe('CaretValueRuleExtractor', () => {
expect(caretRules).toEqual<ExportableCaretValueRule[]>([]);
});

it('should not extract any SD caret rules for context on an Extension', () => {
const caretRules = CaretValueRuleExtractor.processStructureDefinition(
looseExtSD,
defs,
config
);
expect(caretRules).toEqual<ExportableCaretValueRule[]>([]);
});

it('should extract a url-setting caret rules when a non-standard url is included on a StructureDefinition', () => {
const urlSD = cloneDeep(looseSD);
urlSD.url = 'http://diferenturl.com';
Expand Down
34 changes: 34 additions & 0 deletions test/extractor/fixtures/extension-with-context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"resourceType": "StructureDefinition",
"id": "ExtensionWithContext",
"url": "http://hl7.org/fhir/sushi-test/StructureDefinition/ExtensionWithContext",
"name": "ExtensionWithContext",
"fhirVersion": "4.0.1",
"mapping": [
{
"identity": "rim",
"uri": "http://hl7.org/v3",
"name": "RIM Mapping"
}
],
"kind": "complex-type",
"abstract": false,
"context": [
{
"expression": "some.fhirpath",
"type": "fhirpath"
}
],
"type": "Extension",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension",
"derivation": "constraint",
"differential": {
"element": [
{
"id": "Extension.url",
"path": "Extension.url",
"fixedUri": "http://hl7.org/fhir/sushi-test/StructureDefinition/ExtensionWithContext"
}
]
}
}
Loading

0 comments on commit 70a4e78

Please sign in to comment.