Skip to content

Commit

Permalink
Insert RuleSet at code path (#942)
Browse files Browse the repository at this point in the history
* Insert RuleSet at code path

An insert rule on a CodeSystem or RuleSet can start with a code path.
This allows the code path to act as the context for the rules contained
in the inserted RuleSet. When an InsertRule has a code path, it is
applied to ConceptRules and CaretValueRules in the inserted RuleSet. An
InsertRule with a code path on an entity other than a CodeSystem or
RuleSet is syntactically invalid.

* InsertRuleContext contains a code path

When checking if a parser context contains a code path, consider the
presence of a RULESET_REFERENCE or PARAM_RULESET_REFERENCE to be
sufficient. Although this will include insert rules that contain a
regular path, those will also get picked up by containsPathContext, so
there are no consequences.
  • Loading branch information
mint-thompson authored Oct 27, 2021
1 parent 3b0e799 commit f755cd2
Show file tree
Hide file tree
Showing 14 changed files with 1,270 additions and 1,017 deletions.
5 changes: 3 additions & 2 deletions antlr/src/main/antlr/FSH.g4
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ vsMetadata: id | title | description;
vsRule: vsComponent | caretValueRule | insertRule;
codeSystem: KW_CODESYSTEM name csMetadata* csRule*;
csMetadata: id | title | description;
csRule: concept | codeCaretValueRule | insertRule;
csRule: concept | codeCaretValueRule | codeInsertRule;

ruleSet: KW_RULESET RULESET_REFERENCE ruleSetRule+;
ruleSetRule: sdRule | addElementRule | concept | codeCaretValueRule | vsComponent | mappingRule;
ruleSetRule: sdRule | addElementRule | concept | codeCaretValueRule | codeInsertRule | vsComponent | mappingRule;

paramRuleSet: KW_RULESET PARAM_RULESET_REFERENCE paramRuleSetContent;
paramRuleSetContent: STAR
Expand Down Expand Up @@ -75,6 +75,7 @@ caretValueRule: STAR path? caretPath EQUAL value;
codeCaretValueRule: STAR CODE* caretPath EQUAL value;
mappingRule: STAR path? ARROW STRING STRING? CODE?;
insertRule: STAR path? KW_INSERT (RULESET_REFERENCE | PARAM_RULESET_REFERENCE);
codeInsertRule: STAR CODE* KW_INSERT (RULESET_REFERENCE | PARAM_RULESET_REFERENCE);
addElementRule: STAR path CARD flag* targetType (KW_OR targetType)* STRING (STRING | MULTILINE_STRING)?;
pathRule: STAR path;

Expand Down
7 changes: 7 additions & 0 deletions src/fhirtypes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,13 @@ export function applyInsertRules(
}
ruleSetRuleClone.path = newPath;
}
if (rule.pathArray.length > 0) {
if (ruleSetRuleClone instanceof ConceptRule) {
ruleSetRuleClone.hierarchy.unshift(...rule.pathArray);
} else if (ruleSetRuleClone instanceof CaretValueRule) {
ruleSetRuleClone.pathArray.unshift(...rule.pathArray);
}
}
if (ruleSetRuleClone instanceof ConceptRule && fshDefinition instanceof FshCodeSystem) {
// ConceptRules should not have a path context, so if one exists, show an error.
// The concept is still added to the CodeSystem.
Expand Down
9 changes: 8 additions & 1 deletion src/fshtypes/rules/InsertRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Rule } from './Rule';
export class InsertRule extends Rule {
ruleSet: string;
params: string[];
pathArray: string[] = [];

constructor(path: string) {
super(path);
Expand All @@ -22,7 +23,13 @@ export class InsertRule extends Rule {
}

toFSH(): string {
let printablePath: string;
if (this.pathArray.length) {
printablePath = this.pathArray.map(code => `#${code}`).join(' ') + ' ';
} else {
printablePath = this.path !== '' ? `${this.path} ` : '';
}
const paramPart = this.params.length > 0 ? `(${this.fshifyParameters()})` : '';
return `* ${this.path !== '' ? this.path + ' ' : ''}insert ${this.ruleSet}${paramPart}`;
return `* ${printablePath}insert ${this.ruleSet}${paramPart}`;
}
}
143 changes: 76 additions & 67 deletions src/import/FSHImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,8 +1104,8 @@ export class FSHImporter extends FSHVisitor {
return this.visitConcept(ctx.concept());
} else if (ctx.codeCaretValueRule()) {
return this.visitCodeCaretValueRule(ctx.codeCaretValueRule());
} else if (ctx.insertRule()) {
return this.visitInsertRule(ctx.insertRule());
} else if (ctx.codeInsertRule()) {
return this.visitCodeInsertRule(ctx.codeInsertRule());
}
}

Expand Down Expand Up @@ -1626,70 +1626,23 @@ export class FSHImporter extends FSHVisitor {
this.getPathWithContext(this.visitPath(ctx.path()), ctx);
}

visitCodeInsertRule(ctx: pc.CodeInsertRuleContext): InsertRule {
const insertRule = new InsertRule('')
.withLocation(this.extractStartStop(ctx))
.withFile(this.currentFile);
const localCodePath = ctx.CODE().map(code => {
return this.parseCodeLexeme(code.getText(), ctx).code;
});
const fullCodePath = this.getArrayPathWithContext(localCodePath, ctx);
insertRule.pathArray = fullCodePath;
return this.applyRuleSetParams(ctx, insertRule);
}

visitInsertRule(ctx: pc.InsertRuleContext): InsertRule {
const insertRule = new InsertRule(this.getPathWithContext(this.visitPath(ctx.path()), ctx))
.withLocation(this.extractStartStop(ctx))
.withFile(this.currentFile);
const [rulesetName, ruleParams] = this.parseRulesetReference(
ctx.RULESET_REFERENCE()?.getText() ?? ctx.PARAM_RULESET_REFERENCE()?.getText() ?? ''
);
insertRule.ruleSet = rulesetName;
if (ruleParams) {
insertRule.params = this.parseInsertRuleParams(ruleParams);
const ruleSet = this.paramRuleSets.get(insertRule.ruleSet);
if (ruleSet) {
const ruleSetIdentifier = JSON.stringify([ruleSet.name, ...insertRule.params]);
if (ruleSet.parameters.length === insertRule.params.length) {
// no need to create the appliedRuleSet again if we already have it
if (!this.currentDoc.appliedRuleSets.has(ruleSetIdentifier)) {
// create a new document with the substituted parameters
const appliedFsh = `RuleSet: ${ruleSet.name}${EOL}${ruleSet.applyParameters(
insertRule.params
)}${EOL}`;
const appliedRuleSet = this.parseGeneratedRuleSet(
appliedFsh,
ruleSet.name,
ctx,
insertRule
);
if (appliedRuleSet) {
// set the source info based on the original source info
appliedRuleSet.sourceInfo.file = ruleSet.sourceInfo.file;
appliedRuleSet.sourceInfo.location = { ...ruleSet.sourceInfo.location };
appliedRuleSet.rules.forEach(rule => {
rule.sourceInfo.file = appliedRuleSet.sourceInfo.file;
rule.sourceInfo.location.startLine +=
appliedRuleSet.sourceInfo.location.startLine - 1;
rule.sourceInfo.location.endLine +=
appliedRuleSet.sourceInfo.location.startLine - 1;
});
this.currentDoc.appliedRuleSets.set(ruleSetIdentifier, appliedRuleSet);
} else {
logger.error(
`Failed to parse RuleSet ${
insertRule.ruleSet
} with provided parameters (${insertRule.params.join(', ')})`,
insertRule.sourceInfo
);
return;
}
}
} else {
logger.error(
`Incorrect number of parameters applied to RuleSet ${insertRule.ruleSet}`,
insertRule.sourceInfo
);
return;
}
} else {
logger.error(
`Could not find parameterized RuleSet named ${insertRule.ruleSet}`,
insertRule.sourceInfo
);
return;
}
}
return insertRule;
return this.applyRuleSetParams(ctx, insertRule);
}

private parseRulesetReference(reference: string): [string, string] {
Expand Down Expand Up @@ -1747,12 +1700,68 @@ export class FSHImporter extends FSHVisitor {
return paramList.map(param => param.trim());
}

private parseGeneratedRuleSet(
input: string,
name: string,
ctx: pc.InsertRuleContext,
private applyRuleSetParams(
ctx: pc.InsertRuleContext | pc.CodeInsertRuleContext,
insertRule: InsertRule
) {
): InsertRule {
const [rulesetName, ruleParams] = this.parseRulesetReference(
ctx.RULESET_REFERENCE()?.getText() ?? ctx.PARAM_RULESET_REFERENCE()?.getText() ?? ''
);
insertRule.ruleSet = rulesetName;
if (ruleParams) {
insertRule.params = this.parseInsertRuleParams(ruleParams);
const ruleSet = this.paramRuleSets.get(insertRule.ruleSet);
if (ruleSet) {
const ruleSetIdentifier = JSON.stringify([ruleSet.name, ...insertRule.params]);
if (ruleSet.parameters.length === insertRule.params.length) {
// no need to create the appliedRuleSet again if we already have it
if (!this.currentDoc.appliedRuleSets.has(ruleSetIdentifier)) {
// create a new document with the substituted parameters
const appliedFsh = `RuleSet: ${ruleSet.name}${EOL}${ruleSet.applyParameters(
insertRule.params
)}${EOL}`;
const appliedRuleSet = this.parseGeneratedRuleSet(appliedFsh, ruleSet.name, insertRule);
if (appliedRuleSet) {
// set the source info based on the original source info
appliedRuleSet.sourceInfo.file = ruleSet.sourceInfo.file;
appliedRuleSet.sourceInfo.location = { ...ruleSet.sourceInfo.location };
appliedRuleSet.rules.forEach(rule => {
rule.sourceInfo.file = appliedRuleSet.sourceInfo.file;
rule.sourceInfo.location.startLine +=
appliedRuleSet.sourceInfo.location.startLine - 1;
rule.sourceInfo.location.endLine +=
appliedRuleSet.sourceInfo.location.startLine - 1;
});
this.currentDoc.appliedRuleSets.set(ruleSetIdentifier, appliedRuleSet);
} else {
logger.error(
`Failed to parse RuleSet ${
insertRule.ruleSet
} with provided parameters (${insertRule.params.join(', ')})`,
insertRule.sourceInfo
);
return;
}
}
} else {
logger.error(
`Incorrect number of parameters applied to RuleSet ${insertRule.ruleSet}`,
insertRule.sourceInfo
);
return;
}
} else {
logger.error(
`Could not find parameterized RuleSet named ${insertRule.ruleSet}`,
insertRule.sourceInfo
);
return;
}
}
return insertRule;
}

private parseGeneratedRuleSet(input: string, name: string, insertRule: InsertRule) {
// define a temporary document that will contain this RuleSet
const tempDocument = new FSHDocument(this.currentFile);
// save the currentDoc so it can be restored after parsing this RuleSet
Expand Down
3 changes: 2 additions & 1 deletion src/import/generated/FSH.interp

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/import/generated/FSHListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,15 @@ FSHListener.prototype.exitInsertRule = function(ctx) {
};


// Enter a parse tree produced by FSHParser#codeInsertRule.
FSHListener.prototype.enterCodeInsertRule = function(ctx) {
};

// Exit a parse tree produced by FSHParser#codeInsertRule.
FSHListener.prototype.exitCodeInsertRule = function(ctx) {
};


// Enter a parse tree produced by FSHParser#addElementRule.
FSHListener.prototype.enterAddElementRule = function(ctx) {
};
Expand Down
Loading

0 comments on commit f755cd2

Please sign in to comment.