Skip to content

Commit

Permalink
Add support for FHIRPath's endsWith() function. Also, fix behavior of…
Browse files Browse the repository at this point in the history
… startsWith() when used on an empty collection.

PiperOrigin-RevId: 310499325
  • Loading branch information
aaronnash authored and nickgeorge committed May 14, 2020
1 parent a682ace commit 65374ab
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 60 deletions.
115 changes: 55 additions & 60 deletions cc/google/fhir/fhir_path/fhir_path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -676,23 +676,10 @@ class HasValueFunction : public ZeroParameterFunctionNode {
}
};

// Implements the FHIRPath .startsWith() function, which returns true if and
// only if the child string starts with the given string. When the given string
// is the empty string .startsWith() returns true.
//
// Missing or incorrect parameters will end evaluation and cause Evaluate to
// return a status other than OK. See
// http:https://hl7.org/fhirpath/2018Sep/index.html#functions-2.
//
// Please note that execution will proceed on any String-like type.
// Specifically, any type for which its JsonPrimitive value is a string. This
// differs from the allowed implicit conversions defined in
// https://hl7.org/fhirpath/2018Sep/index.html#conversion.
class StartsWithFunction : public SingleValueFunctionNode {
class StringTestFunction : public SingleValueFunctionNode {
public:
explicit StartsWithFunction(
const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
StringTestFunction(const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: SingleValueFunctionNode(child, params) {}

Status EvaluateWithParam(
Expand All @@ -701,74 +688,82 @@ class StartsWithFunction : public SingleValueFunctionNode {
std::vector<WorkspaceMessage> child_results;
FHIR_RETURN_IF_ERROR(child_->Evaluate(work_space, &child_results));

if (child_results.size() != 1) {
return InvalidArgumentError(kInvalidArgumentMessage);
if (child_results.size() > 1) {
return InvalidArgumentError("Function must be invoked on a string.");
}

if (child_results.empty()) {
return absl::OkStatus();
}

FHIR_ASSIGN_OR_RETURN(
std::string item,
MessagesToString(work_space->GetPrimitiveHandler(), child_results));
FHIR_ASSIGN_OR_RETURN(
std::string prefix,
std::string test_string,
MessageToString(work_space->GetPrimitiveHandler(), param));

Message* result = work_space->GetPrimitiveHandler()->NewBoolean(
absl::StartsWith(item, prefix));
Message* result =
work_space->GetPrimitiveHandler()->NewBoolean(Test(item, test_string));
work_space->DeleteWhenFinished(result);
results->push_back(WorkspaceMessage(result));
return absl::OkStatus();
}

virtual bool Test(absl::string_view input,
absl::string_view test_string) const = 0;

const Descriptor* ReturnType() const override {
return Boolean::descriptor();
}
};

private:
static constexpr char kInvalidArgumentMessage[] =
"startsWith must be invoked on a string with a single string "
"argument";
// Implements the FHIRPath .startsWith() function, which returns true if and
// only if the child string starts with the given string. When the given string
// is the empty string .startsWith() returns true.
//
// Missing or incorrect parameters will end evaluation and cause Evaluate to
// return a status other than OK. See
// http:https://hl7.org/fhirpath/2018Sep/index.html#functions-2.
//
// Please note that execution will proceed on any String-like type.
// Specifically, any type for which its JsonPrimitive value is a string. This
// differs from the allowed implicit conversions defined in
// https://hl7.org/fhirpath/2018Sep/index.html#conversion.
class StartsWithFunction : public StringTestFunction {
public:
StartsWithFunction(const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: StringTestFunction(child, params) {}
bool Test(absl::string_view input,
absl::string_view test_string) const override {
return absl::StartsWith(input, test_string);
}
};

// Implements the FHIRPath .endsWith() function.
class EndsWithFunction : public StringTestFunction {
public:
EndsWithFunction(const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: StringTestFunction(child, params) {}
bool Test(absl::string_view input,
absl::string_view test_string) const override {
return absl::EndsWith(input, test_string);
}
};
constexpr char StartsWithFunction::kInvalidArgumentMessage[];

// Implements the FHIRPath .contains() function.
class ContainsFunction : public SingleValueFunctionNode {
class ContainsFunction : public StringTestFunction {
public:
explicit ContainsFunction(
const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: SingleValueFunctionNode(child, params) {}

Status EvaluateWithParam(
WorkSpace* work_space, const WorkspaceMessage& param,
std::vector<WorkspaceMessage>* results) const override {
std::vector<WorkspaceMessage> child_results;
FHIR_RETURN_IF_ERROR(child_->Evaluate(work_space, &child_results));

if (child_results.empty()) {
return absl::OkStatus();
}

if (child_results.size() > 1) {
return InvalidArgumentError(
"contains() must be invoked on a single string.");
}

FHIR_ASSIGN_OR_RETURN(
std::string haystack,
MessagesToString(work_space->GetPrimitiveHandler(), child_results));
FHIR_ASSIGN_OR_RETURN(
std::string needle,
MessageToString(work_space->GetPrimitiveHandler(), param));
: StringTestFunction(child, params) {}

Message* result = work_space->GetPrimitiveHandler()->NewBoolean(
absl::StrContains(haystack, needle));
work_space->DeleteWhenFinished(result);
results->push_back(WorkspaceMessage(result));
return absl::OkStatus();
}

const Descriptor* ReturnType() const override {
return Boolean::descriptor();
bool Test(absl::string_view input,
absl::string_view test_string) const override {
return absl::StrContains(input, test_string);
}
};

Expand Down Expand Up @@ -3445,10 +3440,10 @@ class FhirPathCompilerVisitor : public FhirPathBaseVisitor {
{"toTime", UnimplementedFunction},
{"indexOf", UnimplementedFunction},
{"substring", UnimplementedFunction},
{"endsWith", UnimplementedFunction},
{"upper", UnimplementedFunction},
{"lower", UnimplementedFunction},
{"replace", UnimplementedFunction},
{"endsWith", FunctionNode::Create<EndsWithFunction>},
{"toChars", UnimplementedFunction},
{"today", UnimplementedFunction},
{"now", UnimplementedFunction},
Expand Down
31 changes: 31 additions & 0 deletions cc/google/fhir/fhir_path/fhir_path_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,36 @@ FHIR_VERSION_TEST(FhirPathTest, TestFunctionContains, {
EXPECT_THAT(Evaluate("{}.contains('foo')"), EvalsToEmpty());
})

FHIR_VERSION_TEST(FhirPathTest, TestFunctionEndsWith, {
// Missing argument
EXPECT_THAT(Evaluate("'foo'.endsWith()"),
HasStatusCode(StatusCode::kInvalidArgument));

// Empty colection argument
EXPECT_THAT(Evaluate("'foo'.endsWith({})"),
HasStatusCode(StatusCode::kInvalidArgument));

// Too many arguments
EXPECT_THAT(Evaluate("'foo'.endsWith('foo', 'foo')"),
HasStatusCode(StatusCode::kInvalidArgument));

// Wrong argument type
EXPECT_THAT(Evaluate("'foo'.endsWith(1)"),
HasStatusCode(StatusCode::kInvalidArgument));

// Function does not exist for non-string type
EXPECT_THAT(Evaluate("1.endsWith('1')"),
HasStatusCode(StatusCode::kInvalidArgument));

// Basic cases
EXPECT_THAT(Evaluate("{}.endsWith('')"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.endsWith('')"), EvalsToTrue());
EXPECT_THAT(Evaluate("'foo'.endsWith('')"), EvalsToTrue());
EXPECT_THAT(Evaluate("'foo'.endsWith('o')"), EvalsToTrue());
EXPECT_THAT(Evaluate("'foo'.endsWith('foo')"), EvalsToTrue());
EXPECT_THAT(Evaluate("'foo'.endsWith('bfoo')"), EvalsToFalse());
})

FHIR_VERSION_TEST(FhirPathTest, TestFunctionStartsWith, {
// Missing argument
EXPECT_THAT(Evaluate("'foo'.startsWith()"),
Expand All @@ -597,6 +627,7 @@ FHIR_VERSION_TEST(FhirPathTest, TestFunctionStartsWith, {
HasStatusCode(StatusCode::kInvalidArgument));

// Basic cases
EXPECT_THAT(Evaluate("{}.startsWith('')"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.startsWith('')"), EvalsToTrue());
EXPECT_THAT(Evaluate("'foo'.startsWith('')"), EvalsToTrue());
EXPECT_THAT(Evaluate("'foo'.startsWith('f')"), EvalsToTrue());
Expand Down

0 comments on commit 65374ab

Please sign in to comment.