diff --git a/x/logic/predicate/util.go b/x/logic/predicate/util.go index e8835e04..97908eea 100644 --- a/x/logic/predicate/util.go +++ b/x/logic/predicate/util.go @@ -87,3 +87,49 @@ func AtomBool(b bool) engine.Term { } var AtomNull = engine.NewAtom("@").Apply(engine.NewAtom("null")) + +// ExtractJsonTerm is an utility function that would extract all attribute of a JSON object +// that is represented in prolog with the `json` atom. +// +// This function will ensure the json atom follow our json object representation in prolog. +// +// A JSON object is represented like this : +// +// ``` +// json([foo-bar]) +// ``` +// +// That give a JSON object: `{"foo": "bar"}` +// Returns the map of all attributes with its term value. +func ExtractJsonTerm(term engine.Compound, env *engine.Env) (map[string]engine.Term, error) { + if term.Functor() != AtomJSON { + return nil, fmt.Errorf("invalid functor %s. Expected %s", term.Functor().String(), AtomJSON.String()) + } else if term.Arity() != 1 { + return nil, fmt.Errorf("invalid compound arity : %d but expected %d", term.Arity(), 1) + } + + list := term.Arg(0) + switch l := env.Resolve(list).(type) { + case engine.Compound: + iter := engine.ListIterator{ + List: l, + Env: env, + } + terms := make(map[string]engine.Term, 0) + for iter.Next() { + pair, ok := env.Resolve(iter.Current()).(engine.Compound) + if !ok || pair.Functor() != AtomPair || pair.Arity() != 2 { + return nil, fmt.Errorf("json attributes should be a pair") + } + + key, ok := env.Resolve(pair.Arg(0)).(engine.Atom) + if !ok { + return nil, fmt.Errorf("first pair arg should be an atom") + } + terms[key.String()] = pair.Arg(1) + } + return terms, nil + default: + return nil, fmt.Errorf("json compound should contains one list, give %T", l) + } +} diff --git a/x/logic/predicate/util_test.go b/x/logic/predicate/util_test.go new file mode 100644 index 00000000..4d9b8a0f --- /dev/null +++ b/x/logic/predicate/util_test.go @@ -0,0 +1,80 @@ +package predicate + +import ( + "fmt" + "testing" + + "github.com/ichiban/prolog/engine" + . "github.com/smartystreets/goconvey/convey" +) + +func TestExtractJsonTerm(t *testing.T) { + Convey("Given a test cases", t, func() { + cases := []struct { + compound engine.Compound + result map[string]engine.Term + wantSuccess bool + wantError error + }{ + { + compound: engine.NewAtom("foo").Apply(engine.NewAtom("bar")).(engine.Compound), + wantSuccess: false, + wantError: fmt.Errorf("invalid functor foo. Expected json"), + }, + { + compound: engine.NewAtom("json").Apply(engine.NewAtom("bar"), engine.NewAtom("foobar")).(engine.Compound), + wantSuccess: false, + wantError: fmt.Errorf("invalid compound arity : 2 but expected 1"), + }, + { + compound: engine.NewAtom("json").Apply(engine.NewAtom("bar")).(engine.Compound), + wantSuccess: false, + wantError: fmt.Errorf("json compound should contains one list, give engine.Atom"), + }, + { + compound: AtomJSON.Apply(engine.List(AtomPair.Apply(engine.NewAtom("foo"), engine.NewAtom("bar")))).(engine.Compound), + result: map[string]engine.Term{ + "foo": engine.NewAtom("bar"), + }, + wantSuccess: true, + }, + { + compound: AtomJSON.Apply(engine.List(engine.NewAtom("foo"), engine.NewAtom("bar"))).(engine.Compound), + wantSuccess: false, + wantError: fmt.Errorf("json attributes should be a pair"), + }, + { + compound: AtomJSON.Apply(engine.List(AtomPair.Apply(engine.Integer(10), engine.NewAtom("bar")))).(engine.Compound), + wantSuccess: false, + wantError: fmt.Errorf("first pair arg should be an atom"), + }, + } + for nc, tc := range cases { + Convey(fmt.Sprintf("Given the term compound #%d: %s", nc, tc.compound), func() { + Convey("when extract json term", func() { + env := engine.Env{} + result, err := ExtractJsonTerm(tc.compound, &env) + + if tc.wantSuccess { + Convey("then no error should be thrown", func() { + So(err, ShouldBeNil) + So(result, ShouldNotBeNil) + + Convey("and result should be as expected", func() { + So(result, ShouldResemble, tc.result) + }) + }) + } else { + Convey("then error should occurs", func() { + So(err, ShouldNotBeNil) + + Convey("and should be as expected", func() { + So(err, ShouldResemble, tc.wantError) + }) + }) + } + }) + }) + } + }) +}