From a1d58506740dae5c24fad2559162a67a41357153 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 16 Mar 2023 11:07:33 -0700 Subject: [PATCH] [V2 API] Parse arc into a Graph object (#1057) --- internal/server/v2/graph.go | 30 +++++++ internal/server/v2/parser.go | 72 ++++++++++++++++ internal/server/v2/parser_test.go | 132 ++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 internal/server/v2/graph.go diff --git a/internal/server/v2/graph.go b/internal/server/v2/graph.go new file mode 100644 index 000000000..78bb4b295 --- /dev/null +++ b/internal/server/v2/graph.go @@ -0,0 +1,30 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package v2 is the version 2 of the Data Commons REST API. +package v2 + +// Arc represents an arc in the graph. +type Arc struct { + // Whether it's out or in arc. + out bool + // The property of the arc. This is when property is specified without [] + singleProp string + // The wildcard used for the single property. + wildcard string + // The properties of the arc. This is when property is specified with [] + bracketProps []string + // The filter of the arc. + filter map[string]string +} diff --git a/internal/server/v2/parser.go b/internal/server/v2/parser.go index 90e571472..05a793dd0 100644 --- a/internal/server/v2/parser.go +++ b/internal/server/v2/parser.go @@ -16,6 +16,8 @@ package v2 import ( + "strings" + "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -45,3 +47,73 @@ func splitArc(s string) ([]string, error) { } return parts, nil } + +func parseArc(s string) (*Arc, error) { + if len(s) < 2 { + return nil, status.Errorf( + codes.InvalidArgument, "invalid arc string: %s", s) + } + arc := &Arc{} + if s[0:2] == "->" { + arc.out = true + } else if s[0:2] == "<-" { + arc.out = false + } else { + return nil, status.Errorf( + codes.InvalidArgument, "arc string should start with arrow, %s", s) + } + s = s[2:] + // No property defined; This is to fetch all the properties. + if len(s) == 0 { + return arc, nil + } + // [prop1, prop2] + if s[0] == '[' { + if s[len(s)-1] != ']' { + return nil, status.Errorf( + codes.InvalidArgument, "invalid filter string: %s", s) + } + s = s[1 : len(s)-1] + arc.bracketProps = strings.Split(strings.ReplaceAll(s, " ", ""), ",") + return arc, nil + } + for i := 0; i < len(s); i++ { + if s[i] == '+' { + // <-containedInPlace+ + arc.singleProp = s[0:i] + arc.wildcard = "+" + s = s[i+1:] + break + } + if s[i] == '{' { + // <-containedInPlace{p:v} + arc.singleProp = s[0:i] + s = s[i:] + break + } + } + // {prop1:val1, prop2:val2} + if len(s) > 0 && s[0] == '{' { + if s[len(s)-1] != '}' { + return nil, status.Errorf( + codes.InvalidArgument, "invalid filter string: %s", s) + } + filter := map[string]string{} + parts := strings.Split(strings.ReplaceAll(s[1:len(s)-1], " ", ""), ",") + for _, p := range parts { + kv := strings.Split(p, ":") + if len(kv) != 2 || kv[0] == "" || kv[1] == "" { + return nil, status.Errorf( + codes.InvalidArgument, "invalid filter string: %s", p) + } + filter[kv[0]] = kv[1] + } + arc.filter = filter + return arc, nil + } + // No '+' or '{' found, this is a single property. + if len(s) > 0 { + arc.singleProp = s + } + return arc, nil +} diff --git a/internal/server/v2/parser_test.go b/internal/server/v2/parser_test.go index 67efb6a93..018bc359f 100644 --- a/internal/server/v2/parser_test.go +++ b/internal/server/v2/parser_test.go @@ -68,3 +68,135 @@ func TestSplit(t *testing.T) { } } } + +func TestParseArc(t *testing.T) { + for _, c := range []struct { + s string + arc *Arc + valid bool + }{ + { + "<-", + &Arc{ + out: false, + }, + true, + }, + { + "<-*", + &Arc{ + out: false, + singleProp: "*", + }, + true, + }, + { + "->?", + &Arc{ + out: true, + singleProp: "?", + }, + true, + }, + { + "->#", + &Arc{ + out: true, + singleProp: "#", + }, + true, + }, + { + "->prop1", + &Arc{ + out: true, + singleProp: "prop1", + }, + true, + }, + { + "<-[dcid, displayName, definition]", + &Arc{ + out: false, + bracketProps: []string{"dcid", "displayName", "definition"}, + }, + true, + }, + { + "<-[dcid]", + &Arc{ + out: false, + bracketProps: []string{"dcid"}, + }, + true, + }, + { + "->containedInPlace+", + &Arc{ + out: true, + singleProp: "containedInPlace", + wildcard: "+", + }, + true, + }, + { + "->containedInPlace+{typeOf: City}", + &Arc{ + out: true, + singleProp: "containedInPlace", + wildcard: "+", + filter: map[string]string{ + "typeOf": "City", + }, + }, + true, + }, + { + "<-observationAbout{variableMeasured: Count_Person }", + &Arc{ + out: false, + singleProp: "observationAbout", + filter: map[string]string{ + "variableMeasured": "Count_Person", + }, + }, + true, + }, + { + "<-prop{p:v}", + &Arc{ + out: false, + singleProp: "prop", + filter: map[string]string{ + "p": "v", + }, + }, + true, + }, + { + "<-[dcid", + nil, + false, + }, + { + "<-prop{dcid}", + nil, + false, + }, + } { + result, err := parseArc(c.s) + if !c.valid { + if err == nil { + t.Errorf("parseArc(%s) expect error, but got nil", c.s) + } + continue + } + if err != nil { + t.Errorf("parseArc(%s) got error %v", c.s, err) + continue + } + if diff := cmp.Diff(result, c.arc, cmp.AllowUnexported(Arc{})); diff != "" { + t.Errorf("v(%s) got diff %v", c.s, diff) + } + } +}