Merge pull request #1840 from hapifhir/2024-12-gg-liquid-refactor

2024 12 gg liquid refactor
This commit is contained in:
Grahame Grieve 2024-12-04 06:09:12 +03:00 committed by GitHub
commit 2cdef8f63d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 281 additions and 20 deletions

View File

@ -43,7 +43,7 @@ public class LiquidEngineTests implements ILiquidEngineIncludeResolver {
public static Stream<Arguments> data() throws ParserConfigurationException, SAXException, IOException {
testdoc = (JsonObject) new com.google.gson.JsonParser()
.parse(TestingUtilities.loadTestResource("r5", "liquid", "liquid-tests.json"));
.parse(TestingUtilities.loadTestResource("r4b", "liquid", "liquid-tests.json"));
JsonArray tests = testdoc.getAsJsonArray("tests");
List<Arguments> objects = new ArrayList<>();
for (JsonElement n : tests) {

View File

@ -30,13 +30,13 @@ import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.liquid.LiquidEngine;
import org.hl7.fhir.r5.liquid.LiquidEngine.LiquidDocument;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.r5.model.Tuple;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.utils.EOperationOutcome;
import org.hl7.fhir.r5.utils.LiquidEngine;
import org.hl7.fhir.r5.utils.LiquidEngine.LiquidDocument;
import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.Utilities;

View File

@ -189,6 +189,13 @@ public class FHIRPathEngine {
// the application can implement them by providing a constant resolver
public interface IEvaluationContext {
public abstract class FunctionDefinition {
public abstract String name();
public abstract FunctionDetails details();
public abstract TypeDetails check(FHIRPathEngine engine, Object appContext, TypeDetails focus, List<TypeDetails> parameters);
public abstract List<Base> execute(FHIRPathEngine engine, Object appContext, List<Base> focus, List<List<Base>> parameters);
}
/**
* A constant reference - e.g. a reference to a name that must be resolved in context.
* The % will be removed from the constant name before this is invoked.

View File

@ -1,4 +1,4 @@
package org.hl7.fhir.r5.utils;
package org.hl7.fhir.r5.liquid;
import java.util.List;

View File

@ -1,4 +1,4 @@
package org.hl7.fhir.r5.utils;
package org.hl7.fhir.r5.liquid;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.Base;
@ -83,5 +83,15 @@ public class BaseJsonWrapper extends Base {
}
}
@Override
public boolean isPrimitive() {
return j.isJsonPrimitive();
}
@Override
public String primitiveValue() {
return toString();
}
}

View File

@ -0,0 +1,108 @@
package org.hl7.fhir.r5.liquid;
import java.util.ArrayList;
import java.util.List;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext.FunctionDefinition;
import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
import org.hl7.fhir.r5.fhirpath.TypeDetails;
import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.IntegerType;
import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.Utilities;
import com.microsoft.schemas.office.visio.x2012.main.impl.FunctionDefTypeImpl;
public class GlobalObject extends Base {
private DateTimeType dt;
private StringType pathToSpec;
public GlobalObject(DateTimeType td, StringType pathToSpec) {
super();
this.dt = td;
this.pathToSpec = pathToSpec;
}
@Override
public String fhirType() {
return "GlobalObject";
}
@Override
public String getIdBase() {
return null;
}
@Override
public void setIdBase(String value) {
throw new Error("Read only");
}
@Override
public Base copy() {
return this;
}
@Override
public FhirPublication getFHIRPublicationVersion() {
return FhirPublication.R5;
}
public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException {
if ("dateTime".equals(name)) {
return wrap(dt);
} else if ("path".equals(name)) {
return wrap(pathToSpec);
} else {
return super.getProperty(hash, name, checkValid);
}
}
private Base[] wrap(Base b) {
Base[] l = new Base[1];
l[0] = b;
return l;
}
@Override
public List<Base> executeFunction(FHIRPathEngine engine, Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) {
return null;
}
public static class GlobalObjectRandomFunction extends FunctionDefinition {
@Override
public String name() {
return "random";
}
@Override
public FunctionDetails details() {
return new FunctionDetails("Generate a Random Number", 1, 1);
}
@Override
public TypeDetails check(FHIRPathEngine engine, Object appContext, TypeDetails focus, List<TypeDetails> parameters) {
if (focus.hasType("GlobalObject")) {
return new TypeDetails(CollectionStatus.SINGLETON, "integer");
} else {
return null;
}
}
@Override
public List<Base> execute(FHIRPathEngine engine, Object appContext, List<Base> focus, List<List<Base>> parameters) {
List<Base> list = new ArrayList<>();
int scale = Utilities.parseInt(parameters.get(0).get(0).primitiveValue(), 100)+ 1;
list.add(new IntegerType((int)(Math.random() * scale)));
return list;
}
}
}

View File

@ -1,4 +1,4 @@
package org.hl7.fhir.r5.utils;
package org.hl7.fhir.r5.liquid;
import java.util.ArrayList;
import java.util.Arrays;
@ -42,22 +42,114 @@ import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.fhirpath.ExpressionNode;
import org.hl7.fhir.r5.fhirpath.FHIRLexer;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
import org.hl7.fhir.r5.fhirpath.TypeDetails;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.ExpressionNodeWithOffset;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
import org.hl7.fhir.r5.fhirpath.TypeDetails;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.BooleanType;
import org.hl7.fhir.r5.model.IntegerType;
import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.r5.model.Tuple;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.MarkDownProcessor;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.MarkDownProcessor.Dialect;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
public class LiquidEngine implements IEvaluationContext {
public static class LiquidForLoopObject extends Base {
private static final long serialVersionUID = 6951452522873320076L;
private boolean first;
private int index;
private int index0;
private int rindex;
private int rindex0;
private boolean last;
private int length;
private LiquidForLoopObject parentLoop;
public LiquidForLoopObject(int size, int i, int offset, int limit, LiquidForLoopObject parentLoop) {
super();
this.parentLoop = parentLoop;
if (offset == -1) {
offset = 0;
}
if (limit == -1) {
limit = size;
}
first = i == offset;
index = i+1-offset;
index0 = i-offset;
rindex = (limit-offset) - 1 - i;
rindex0 = (limit-offset) - i;
length = limit-offset;
last = i == (limit-offset)-1;
}
@Override
public String getIdBase() {
return null;
}
@Override
public void setIdBase(String value) {
throw new Error("forLoop is read only");
}
@Override
public Base copy() {
throw new Error("forLoop is read only");
}
@Override
public FhirPublication getFHIRPublicationVersion() {
return FhirPublication.R5;
}
public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException {
switch (name) {
case "parentLoop" : return wrap(parentLoop);
case "first" : return wrap(new BooleanType(first));
case "last" : return wrap(new BooleanType(last));
case "index" : return wrap(new IntegerType(index));
case "index0" : return wrap(new IntegerType(index0));
case "rindex" : return wrap(new IntegerType(rindex));
case "rindex0" : return wrap(new IntegerType(rindex0));
case "length" : return wrap(new IntegerType(length));
}
return super.getProperty(hash, name, checkValid);
}
private Base[] wrap(Base b) {
Base[] l = new Base[1];
l[0] = b;
return l;
}
@Override
public String toString() {
return "forLoop";
}
@Override
public String fhirType() {
return "ForLoop";
}
}
public interface ILiquidRenderingSupport {
String renderForLiquid(Object appContext, Base i) throws FHIRException;
}
@ -71,16 +163,18 @@ public class LiquidEngine implements IEvaluationContext {
private ILiquidEngineIncludeResolver includeResolver;
private ILiquidRenderingSupport renderingSupport;
private MarkDownProcessor processor = new MarkDownProcessor(Dialect.COMMON_MARK);
private Map<String, Base> vars = new HashMap<>();
private class LiquidEngineContext {
private Object externalContext;
private Map<String, Base> loopVars = new HashMap<>();
private Map<String, Base> globalVars = new HashMap<>();
public LiquidEngineContext(Object externalContext) {
public LiquidEngineContext(Object externalContext, Map<String, Base> vars) {
super();
this.externalContext = externalContext;
globalVars = new HashMap<>();
globalVars.putAll(vars);
}
public LiquidEngineContext(Object externalContext, LiquidEngineContext existing) {
@ -122,13 +216,17 @@ public class LiquidEngine implements IEvaluationContext {
this.renderingSupport = renderingSupport;
}
public Map<String, Base> getVars() {
return vars;
}
public LiquidDocument parse(String source, String sourceName) throws FHIRException {
return new LiquidParser(source).parse(sourceName);
}
public String evaluate(LiquidDocument document, Base resource, Object appContext) throws FHIRException {
StringBuilder b = new StringBuilder();
LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
LiquidEngineContext ctxt = new LiquidEngineContext(appContext, vars );
for (LiquidNode n : document.body) {
n.evaluate(b, resource, ctxt);
}
@ -403,6 +501,7 @@ public class LiquidEngine implements IEvaluationContext {
Collections.reverse(list);
}
int i = 0;
LiquidForLoopObject parentLoop = (LiquidForLoopObject) lctxt.globalVars.get("forLoop");
for (Base o : list) {
if (offset >= 0 && i < offset) {
i++;
@ -411,6 +510,8 @@ public class LiquidEngine implements IEvaluationContext {
if (limit >= 0 && i == limit) {
break;
}
LiquidForLoopObject forloop = new LiquidForLoopObject(list.size(), i, offset, limit, parentLoop);
lctxt.globalVars.put("forLoop", forloop);
if (lctxt.globalVars.containsKey(varName)) {
throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_VARIABLE_ALREADY_ASSIGNED, varName));
}
@ -431,6 +532,7 @@ public class LiquidEngine implements IEvaluationContext {
}
i++;
}
lctxt.globalVars.put("forLoop", parentLoop);
}
}
@ -477,6 +579,20 @@ public class LiquidEngine implements IEvaluationContext {
}
}
private class LiquidCapture extends LiquidNode {
private String varName;
private List<LiquidNode> body = new ArrayList<>();
@Override
public void evaluate(StringBuilder b, Base resource, LiquidEngineContext ctxt) throws FHIRException {
StringBuilder bc = new StringBuilder();
for (LiquidNode n : body) {
n.evaluate(bc, resource, ctxt);
}
ctxt.globalVars.put(varName, new StringType(bc.toString()));
}
}
private class LiquidInclude extends LiquidNode {
private String page;
private Map<String, ExpressionNode> params = new HashMap<>();
@ -600,6 +716,8 @@ public class LiquidEngine implements IEvaluationContext {
list.add(parseInclude(cnt.substring(7).trim()));
else if (cnt.startsWith("assign "))
list.add(parseAssign(cnt.substring(6).trim()));
else if (cnt.startsWith("capture "))
list.add(parseCapture(cnt.substring(7).trim()));
else
throw new FHIRException(engine.getWorker().formatMessage(I18nConstants.LIQUID_UNKNOWN_FLOW_STMT,name, cnt));
} else { // next2() == '{'
@ -728,6 +846,16 @@ public class LiquidEngine implements IEvaluationContext {
return res;
}
private LiquidNode parseCapture(String cnt) throws FHIRException {
int i = 0;
while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
i++;
LiquidCapture res = new LiquidCapture();
res.varName = cnt.substring(0, i);
parseList(res.body, true, new String[] { "endcapture" });
return res;
}
private LiquidNode parseAssign(String cnt) throws FHIRException {
int i = 0;
while (!Character.isWhitespace(cnt.charAt(i)))

View File

@ -36,7 +36,7 @@ import java.util.Map;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -629,4 +629,10 @@ public abstract class Base implements Serializable, IBase, IElement {
}
public abstract FhirPublication getFHIRPublicationVersion();
public List<Base> executeFunction(FHIRPathEngine engine, Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) {
return new ArrayList<>();
}
}

View File

@ -7,14 +7,14 @@ import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.liquid.LiquidEngine;
import org.hl7.fhir.r5.liquid.LiquidEngine.ILiquidRenderingSupport;
import org.hl7.fhir.r5.liquid.LiquidEngine.LiquidDocument;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.DataType;
import org.hl7.fhir.r5.renderers.utils.RenderingContext;
import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
import org.hl7.fhir.r5.utils.EOperationOutcome;
import org.hl7.fhir.r5.utils.LiquidEngine;
import org.hl7.fhir.r5.utils.LiquidEngine.ILiquidRenderingSupport;
import org.hl7.fhir.r5.utils.LiquidEngine.LiquidDocument;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;

View File

@ -11,11 +11,11 @@ import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.collections4.map.HashedMap;
import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.formats.XmlParser;
import org.hl7.fhir.r5.liquid.LiquidEngine;
import org.hl7.fhir.r5.liquid.LiquidEngine.ILiquidEngineIncludeResolver;
import org.hl7.fhir.r5.liquid.LiquidEngine.LiquidDocument;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.test.utils.TestingUtilities;
import org.hl7.fhir.r5.utils.LiquidEngine;
import org.hl7.fhir.r5.utils.LiquidEngine.ILiquidEngineIncludeResolver;
import org.hl7.fhir.r5.utils.LiquidEngine.LiquidDocument;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;

View File

@ -12,11 +12,13 @@ import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails;
import org.hl7.fhir.r5.liquid.BaseJsonWrapper;
import org.hl7.fhir.r5.liquid.LiquidEngine;
import org.hl7.fhir.r5.liquid.LiquidEngine.LiquidDocument;
import org.hl7.fhir.r5.fhirpath.TypeDetails;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.test.utils.TestingUtilities;
import org.hl7.fhir.r5.utils.LiquidEngine.LiquidDocument;
import org.hl7.fhir.utilities.json.JsonException;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.json.parser.JsonParser;

View File

@ -4320,7 +4320,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
Collections.sort(list, new StructureDefinitionSorterByUrl());
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
for (StructureDefinition sd : list) {
b.append(sd.getUrl());
b.append(sd.getVersionedUrl());
}
return b.toString();
}