fixing broken links, and r4/r5 map testing

This commit is contained in:
Grahame Grieve 2023-03-14 13:24:00 +11:00
parent b5bbe5108b
commit 824f51f82e
6 changed files with 196 additions and 21 deletions

View File

@ -323,48 +323,48 @@ public class AdditionalBindingsRenderer {
boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion());
switch (purpose) {
case "maximum":
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-maximum" : corePath+"extension-elementdefinition-maxvalueset.html", "A required binding, for use when the binding strength is 'extensible' or 'preferred'").tx("Max Binding");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-maximum" : corePath+"extension-elementdefinition-maxvalueset.html", "A required binding, for use when the binding strength is 'extensible' or 'preferred'").tx("Max Binding");
break;
case "minimum":
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-minimum" : corePath+"extension-elementdefinition-minvalueset.html", "The minimum allowable value set - any conformant system SHALL support all these codes").tx("Min Binding");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-minimum" : corePath+"extension-elementdefinition-minvalueset.html", "The minimum allowable value set - any conformant system SHALL support all these codes").tx("Min Binding");
break;
case "required" :
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-required" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = required)").tx("Required Binding");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-required" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = required)").tx("Required Binding");
break;
case "extensible" :
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-extensible" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = extensible)").tx("Extensible Binding");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-extensible" : corePath+"terminologies.html#strength", "Validators will check this binding (strength = extensible)").tx("Extensible Binding");
break;
case "current" :
if (r5) {
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-current" : corePath+"terminologies.html#strength", "New records are required to use this value set, but legacy records may use other codes").tx("Current Binding");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-current" : corePath+"terminologies.html#strength", "New records are required to use this value set, but legacy records may use other codes").tx("Current Binding");
} else {
td.span(null, "New records are required to use this value set, but legacy records may use other codes").tx("Required");
}
break;
case "preferred" :
if (r5) {
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-preferred" : corePath+"terminologies.html#strength", "This is the value set that is recommended (documentation should explain why)").tx("Preferred Binding");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-preferred" : corePath+"terminologies.html#strength", "This is the value set that is recommended (documentation should explain why)").tx("Preferred Binding");
} else {
td.span(null, "This is the value set that is recommended (documentation should explain why)").tx("Recommended");
}
break;
case "ui" :
if (r5) {
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-ui" : corePath+"terminologies.html#strength", "This value set is provided to user look up in a given context").tx("UI Binding");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-ui" : corePath+"terminologies.html#strength", "This value set is provided to user look up in a given context").tx("UI Binding");
} else {
td.span(null, "This value set is provided to user look up in a given context").tx("UI");
}
break;
case "starter" :
if (r5) {
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-starter" : corePath+"terminologies.html#strength", "This value set is a good set of codes to start with when designing your system").tx("Starter Set");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-starter" : corePath+"terminologies.html#strength", "This value set is a good set of codes to start with when designing your system").tx("Starter Set");
} else {
td.span(null, "This value set is a good set of codes to start with when designing your system").tx("Starter");
}
break;
case "component" :
if (r5) {
td.ah(r5 ? "valueset-additional-binding-purpose.html#additional-binding-purpose-component" : corePath+"terminologies.html#strength", "This value set is a component of the base value set").tx("Component");
td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-component" : corePath+"terminologies.html#strength", "This value set is a component of the base value set").tx("Component");
} else {
td.span(null, "This value set is a component of the base value set").tx("Component");
}

View File

@ -243,8 +243,9 @@ public abstract class ParserBase {
return logical;
}
public void setLogical(StructureDefinition logical) {
public ParserBase setLogical(StructureDefinition logical) {
this.logical = logical;
return this;
}
}

View File

@ -2761,4 +2761,30 @@ public class StructureMapUtilities {
return null;
}
public ResolvedGroup getGroupForUrl(StructureMap map, String url, StructureMapInputMode mode) {
for (StructureMapGroupComponent grp : map.getGroup()) {
if (grp.getTypeMode() != StructureMapGroupTypeMode.NULL) {
for (StructureMapGroupInputComponent p : grp.getInput()) {
if (mode == null || mode == p.getMode()) {
String t = resolveInputType(p, map);
if (url.equals(t)) {
return new ResolvedGroup(map, grp);
}
}
}
}
}
return null;
}
public String getInputType(ResolvedGroup grp, StructureMapInputMode mode) {
if (grp.getTargetGroup().getInput().size() != 2 || grp.getTargetGroup().getInput().get(0).getMode() == grp.getTargetGroup().getInput().get(1).getMode()) {
return null;
} else if (grp.getTargetGroup().getInput().get(0).getMode() == mode) {
return resolveInputType(grp.getTargetGroup().getInput().get(0), grp.getTargetMap());
} else {
return resolveInputType(grp.getTargetGroup().getInput().get(1), grp.getTargetMap());
}
}
}

View File

@ -47,7 +47,7 @@ public class ValidatorUtils {
}
}
protected static IWorkerContext.IContextResourceLoader loaderForVersion(String version) {
public static IWorkerContext.IContextResourceLoader loaderForVersion(String version) {
if (Utilities.noString(version)) {
return null;
}

View File

@ -1,26 +1,96 @@
package org.hl7.fhir.validation.special;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
import org.hl7.fhir.r5.model.StructureMap;
import org.hl7.fhir.r5.model.StructureMap.StructureMapInputMode;
import org.hl7.fhir.convertors.loaders.loaderR5.R4ToR5Loader;
import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r4.formats.IParser.OutputStyle;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ResourceFactory;
import org.hl7.fhir.r4.test.utils.TestingUtilities;
import org.hl7.fhir.r5.context.SimpleWorkerContext;
import org.hl7.fhir.r5.context.IWorkerContext.IContextResourceLoader;
import org.hl7.fhir.r5.context.SimpleWorkerContext.SimpleWorkerContextBuilder;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.utils.structuremap.ResolvedGroup;
import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities;
import org.hl7.fhir.r5.utils.structuremap.VariableMode;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.json.JsonException;
import org.hl7.fhir.utilities.json.model.JsonElement;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.json.model.JsonProperty;
import org.hl7.fhir.utilities.json.parser.JsonParser;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.NpmPackage;
import org.hl7.fhir.validation.IgLoader;
import org.hl7.fhir.validation.ValidatorUtils;
import org.hl7.fhir.validation.special.R4R5MapTester.Stats;
public class R4R5MapTester {
public class Stats {
private Set<String> errors = new HashSet<>();
private int total;
private int parsed;
private int forward;
public void example() {
total++;
}
public void parsed() {
parsed++;
}
public void error(String s) {
errors.add(s);
}
public void forward() {
forward++;
}
public int totalCount() {
return total;
}
public int parseCount() {
return parsed;
}
public String summary() {
if (errors.size() == 0) {
return "All OK";
} else {
return String.join(", ", errors);
}
}
public boolean ok() {
return errors.size() == 0;
}
}
private boolean saveProcess = false;
private SimpleWorkerContext context;
private FilesystemPackageCacheManager pcm;
private StructureMapUtilities utils;
@ -32,18 +102,29 @@ public class R4R5MapTester {
new R4R5MapTester().testMaps(args[0]);
}
private void testMaps(String src) throws JsonException, IOException {
public void testMaps(String src) throws JsonException, IOException {
log("Load Test Outcomes");
JsonObject json = JsonParser.parseObjectFromFile(Utilities.path(src, "input", "_data", "conversions.json"));
log("Load R5");
pcm = new FilesystemPackageCacheManager(true);
context = new SimpleWorkerContextBuilder().withAllowLoadingDuplicates(true).fromPackage(pcm.loadPackage("hl7.fhir.r5.core#current"));
log("Load Maps");
context.loadFromPackage(pcm.loadPackage("hl7.fhir.uv.extensions#dev"), null);
// context.loadFromPackage(pcm.loadPackage(), null);
loadPackage("hl7.fhir.uv.extensions#dev");
loadPackage("hl7.fhir.r4.core#4.0.1");
loadPackage("hl7.fhir.r4b.core#4.3.0");
log("Load R4 Examples");
NpmPackage r4Examples = pcm.loadPackage("hl7.fhir.r4.examples");
log("Load R4B Examples");
NpmPackage r4bExamples = pcm.loadPackage("hl7.fhir.r4b.examples");
utils = new StructureMapUtilities(context);
allMaps = context.fetchResourcesByType(StructureMap.class);
log("Resource Count = "+context.getResourceNames().size());
log("Go. "+context.getResourceNames().size()+" types of resources");
log("Map Count = "+allMaps.size());
boolean changed = false;
for (JsonProperty jp : json.getProperties()) {
@ -53,8 +134,8 @@ public class R4R5MapTester {
StructureDefinition sd = context.fetchTypeDefinition(rn);
List<StructureMap> mapSrc = utils.getMapsForUrl(allMaps, sd.getUrl(), StructureMapInputMode.SOURCE);
List<StructureMap> mapTgt = utils.getMapsForUrl(allMaps, sd.getUrl(), StructureMapInputMode.TARGET);
changed = checkMaps(rn, o.getJsonObject("r4"), "http://hl7.org/fhir/4.0", mapSrc, mapTgt) || changed;
changed = checkMaps(rn, o.getJsonObject("r4b"), "http://hl7.org/fhir/4.0", mapSrc, mapTgt) || changed;
changed = checkMaps(sd, o.getJsonObject("r4"), "http://hl7.org/fhir/4.0", mapSrc, mapTgt, r4Examples) || changed;
changed = checkMaps(sd, o.getJsonObject("r4b"), "http://hl7.org/fhir/4.3", mapSrc, mapTgt, r4bExamples) || changed;
}
if (changed) {
JsonParser.compose(json, new FileOutputStream(Utilities.path(src, "input", "_data", "conversions.json")), true);
@ -76,7 +157,15 @@ public class R4R5MapTester {
}
private boolean checkMaps(String rn, JsonObject json, String ns, List<StructureMap> mapSrc, List<StructureMap> mapTgt) {
private void loadPackage(String pid) throws FHIRException, IOException {
log("Load "+pid);
NpmPackage npm = pcm.loadPackage(pid);
IContextResourceLoader loader = ValidatorUtils.loaderForVersion(npm.fhirVersion());
loader.setPatchUrls(VersionUtilities.isCorePackage(npm.id()));
int count = context.loadFromPackage(npm, loader);
}
private boolean checkMaps(StructureDefinition sd, JsonObject json, String ns, List<StructureMap> mapSrc, List<StructureMap> mapTgt, NpmPackage examples) throws IOException {
List<StructureMap> src = utils.getMapsForUrlPrefix(mapSrc, ns, StructureMapInputMode.TARGET);
List<StructureMap> tgt = utils.getMapsForUrlPrefix(mapTgt, ns, StructureMapInputMode.SOURCE);
if (src.size() + tgt.size() == 0) {
@ -92,9 +181,30 @@ public class R4R5MapTester {
isDraft = map.getStatus() == PublicationStatus.DRAFT || isDraft;
}
json.set("status", ""+(src.size()+tgt.size())+" Maps Defined"+(isDraft ? " (draft)" : ""));
if (context.getResourceNames().contains(rn)) {
json.set("testColor", "#ffcccc");
json.set("testMessage", "To Do");
json.set("testColor", "#ffcccc");
if (sd.getKind() == StructureDefinitionKind.RESOURCE) {
if (tgt.size() == 1 && src.size() == 1) {
StructureMap tgtM = tgt.get(0);
ResolvedGroup tgtG = utils.getGroupForUrl(tgtM, sd.getUrl(), StructureMapInputMode.TARGET);
String tgtU = utils.getInputType(tgtG, StructureMapInputMode.SOURCE);
assert tgtU.startsWith(ns);
StructureMap srcM = src.get(0);
ResolvedGroup srcG = utils.getGroupForUrl(srcM, sd.getUrl(), StructureMapInputMode.SOURCE);
String srcU = utils.getInputType(srcG, StructureMapInputMode.TARGET);
assert srcU.startsWith(ns);
if (!srcU.equals(tgtU)) {
json.set("testMessage", "Maps do not round trip to same resource ("+Utilities.tail(srcU)+" -> "+Utilities.tail(tgtU)+") - can't test");
} else {
StructureDefinition tsd = context.fetchResource(StructureDefinition.class, srcU);
if (tsd == null) {
json.set("testMessage", "Undefined type "+srcU);
} else {
testRoundTrips(sd, json, tgtG, srcG, tsd, examples);
}
}
} else {
json.set("testMessage", "Multiple matching maps ("+src.size()+"/"+tgt.size()+") - no tests performed");
}
} else {
json.set("testColor", "#eeeeee");
json.set("testMessage", "n/a");
@ -103,6 +213,44 @@ public class R4R5MapTester {
return true;
}
private void testRoundTrips(StructureDefinition sd, JsonObject json, ResolvedGroup tgtG, ResolvedGroup srcG, StructureDefinition tsd, NpmPackage examples) throws IOException {
Stats stats = new Stats();
for (String s : examples.listResources(tsd.getType())) {
log(" Test "+examples.id()+"::"+s);
try {
testRoundTrip(sd, tsd, tgtG, srcG, stats, examples.load("package", s));
} catch (Exception e) {
log("error: "+e.getMessage());
stats.error("Error: "+e.getMessage());
}
}
json.set("total", stats.totalCount());
json.set("parsed", stats.parseCount());
json.set("testMessage", stats.summary());
if (stats.ok()) {
json.set("testColor", "#d4ffdf");
}
}
private void testRoundTrip(StructureDefinition sd, StructureDefinition tsd, ResolvedGroup tgtG, ResolvedGroup srcG, Stats stats, InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
stats.example();
Element r4 = new org.hl7.fhir.r5.elementmodel.JsonParser(context).setLogical(tsd).parseSingle(stream);
stats.parsed();
String id = r4.getIdBase();
checkSave(id, "src.loaded", r4);
Resource r5 = ResourceFactory.createResource(sd.getType());
utils.transform(context, r4, tgtG.getTargetMap(), r4);
stats.forward();
}
private void checkSave(String id, String state, Element e) {
if (saveProcess) {
// new org.hl7.fhir.r4.elementmodel.JsonParser(context).compose(r3, bso, OutputStyle.PRETTY, null);
}
}
private void log(String msg) {
System.out.println(msg);
}

View File

@ -19,7 +19,7 @@
<properties>
<hapi_fhir_version>6.2.1</hapi_fhir_version>
<validator_test_case_version>1.2.20</validator_test_case_version>
<validator_test_case_version>1.2.21-SNAPSHOT</validator_test_case_version>
<junit_jupiter_version>5.7.1</junit_jupiter_version>
<junit_platform_launcher_version>1.8.2</junit_platform_launcher_version>
<maven_surefire_version>3.0.0-M5</maven_surefire_version>