From b260e21abe103ec6797c86b8c1f0bb2455e567da Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Wed, 26 Apr 2023 14:23:17 +0200 Subject: [PATCH 01/20] test(logic): json_prolog object and string test --- x/logic/predicate/json_test.go | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 x/logic/predicate/json_test.go diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go new file mode 100644 index 00000000..277bd288 --- /dev/null +++ b/x/logic/predicate/json_test.go @@ -0,0 +1,99 @@ +//nolint:gocognit,lll +package predicate + +import ( + "fmt" + "testing" + + tmdb "github.com/cometbft/cometbft-db" + "github.com/cometbft/cometbft/libs/log" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ichiban/prolog/engine" + "github.com/okp4/okp4d/x/logic/testutil" + "github.com/okp4/okp4d/x/logic/types" + . "github.com/smartystreets/goconvey/convey" +) + +func TestJsonProlog(t *testing.T) { + Convey("Given a test cases", t, func() { + cases := []struct { + program string + query string + wantResult []types.TermResults + wantError error + wantSuccess bool + }{ + { + query: `json_prolog('"foo"', Term).`, + wantResult: []types.TermResults{{ + "Term": "foo", + }}, + wantSuccess: true, + }, + { + query: `json_prolog('{"foo": "bar"}', Term).`, + wantResult: []types.TermResults{{ + "Term": "json([-(foo, 'bar')])", + }}, + wantSuccess: true, + }, + } + for nc, tc := range cases { + Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { + Convey("and a context", func() { + db := tmdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db) + ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) + + Convey("and a vm", func() { + interpreter := testutil.NewLightInterpreterMust(ctx) + interpreter.Register2(engine.NewAtom("json_prolog"), JsonProlog) + + err := interpreter.Compile(ctx, tc.program) + So(err, ShouldBeNil) + + Convey("When the predicate is called", func() { + sols, err := interpreter.QueryContext(ctx, tc.query) + + Convey("Then the error should be nil", func() { + So(err, ShouldBeNil) + So(sols, ShouldNotBeNil) + + Convey("and the bindings should be as expected", func() { + var got []types.TermResults + for sols.Next() { + m := types.TermResults{} + err := sols.Scan(m) + So(err, ShouldBeNil) + + got = append(got, m) + } + if tc.wantError != nil { + So(sols.Err(), ShouldNotBeNil) + So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) + } else { + So(sols.Err(), ShouldBeNil) + + if tc.wantSuccess { + So(len(got), ShouldBeGreaterThan, 0) + So(len(got), ShouldEqual, len(tc.wantResult)) + for iGot, resultGot := range got { + for varGot, termGot := range resultGot { + So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.wantResult[iGot][varGot]) + } + } + } else { + So(len(got), ShouldEqual, 0) + } + } + }) + }) + }) + }) + }) + }) + } + }) +} From e43010d6869204e21b392ce54a7be362c359e522 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Wed, 26 Apr 2023 14:23:39 +0200 Subject: [PATCH 02/20] feat(logic): convert json string to terms --- x/logic/predicate/json.go | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 x/logic/predicate/json.go diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go new file mode 100644 index 00000000..7028f62c --- /dev/null +++ b/x/logic/predicate/json.go @@ -0,0 +1,52 @@ +package predicate + +import ( + "encoding/json" + "fmt" + + "github.com/ichiban/prolog/engine" + "github.com/okp4/okp4d/x/logic/util" +) + +// AtomJSON is a term which represents a json as a compound term `json([Pair])`. +var AtomJSON = engine.NewAtom("json") + +// JsonProlog is a predicate that will unify a JSON string into prolog terms and vice versa. +// +// json_prolog(?Json, ?Term) is det +// TODO: +func JsonProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { + switch t1 := env.Resolve(j).(type) { + case engine.Variable: + case engine.Atom: + terms, err := jsonStringToTerms(t1.String()) + if err != nil { + return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + } + + return engine.Unify(vm, term, terms, cont, env) + default: + return engine.Error(fmt.Errorf("did_components/2: cannot unify json with %T", t1)) + } + + switch env.Resolve(term).(type) { + default: + return engine.Error(fmt.Errorf("json_prolog/2: not implemented")) + } +} + +func jsonStringToTerms(j string) (engine.Term, error) { + var values any + json.Unmarshal([]byte(j), &values) + + return jsonToTerms(values) +} + +func jsonToTerms(value any) (engine.Term, error) { + switch v := value.(type) { + case string: + return util.StringToTerm(v), nil + default: + return nil, fmt.Errorf("could not convert %s (%T) to a prolog term", v, v) + } +} From 310459a2e351720c6497d78fb9462a5462cff9b7 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Wed, 26 Apr 2023 16:30:02 +0200 Subject: [PATCH 03/20] feat(logic): convert basic json object into prolog --- x/logic/predicate/json.go | 16 ++++++++++++ x/logic/predicate/json_test.go | 45 +++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 7028f62c..bd3af008 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -3,9 +3,11 @@ package predicate import ( "encoding/json" "fmt" + "sort" "github.com/ichiban/prolog/engine" "github.com/okp4/okp4d/x/logic/util" + "github.com/samber/lo" ) // AtomJSON is a term which represents a json as a compound term `json([Pair])`. @@ -46,6 +48,20 @@ func jsonToTerms(value any) (engine.Term, error) { switch v := value.(type) { case string: return util.StringToTerm(v), nil + case map[string]any: + keys := lo.Keys(v) + sort.Strings(keys) + + attributes := make([]engine.Term, 0, len(v)) + for _, key := range keys { + attributeValue, err := jsonToTerms(v[key]) + if err != nil { + return nil, err + } + attributes = append(attributes, AtomPair.Apply(engine.NewAtom(key), attributeValue)) + } + + return AtomJSON.Apply(engine.List(attributes...)), nil default: return nil, fmt.Errorf("could not convert %s (%T) to a prolog term", v, v) } diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 277bd288..39343a73 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -19,23 +19,62 @@ import ( func TestJsonProlog(t *testing.T) { Convey("Given a test cases", t, func() { cases := []struct { + description string program string query string wantResult []types.TermResults wantError error wantSuccess bool }{ + // ** JSON -> Prolog ** + // String { - query: `json_prolog('"foo"', Term).`, + description: "convert direct string (valid json) into prolog", + query: `json_prolog('"foo"', Term).`, wantResult: []types.TermResults{{ "Term": "foo", }}, wantSuccess: true, }, { - query: `json_prolog('{"foo": "bar"}', Term).`, + description: "convert direct string with space (valid json) into prolog", + query: `json_prolog('"a string with space"', Term).`, wantResult: []types.TermResults{{ - "Term": "json([-(foo, 'bar')])", + "Term": "'a string with space'", + }}, + wantSuccess: true, + }, + // ** JSON -> Prolog ** + // Object + { + description: "convert json object into prolog", + query: `json_prolog('{"foo": "bar"}', Term).`, + wantResult: []types.TermResults{{ + "Term": "json([foo-bar])", + }}, + wantSuccess: true, + }, + { + description: "convert json object with multiple attribute into prolog", + query: `json_prolog('{"foo": "bar", "foobar": "bar foo"}', Term).`, + wantResult: []types.TermResults{{ + "Term": "json([foo-bar,foobar-'bar foo'])", + }}, + wantSuccess: true, + }, + { + description: "convert json object with attribute with a space into prolog", + query: `json_prolog('{"string with space": "bar"}', Term).`, + wantResult: []types.TermResults{{ + "Term": "json(['string with space'-bar])", + }}, + wantSuccess: true, + }, + { + description: "ensure determinism on object attribute key sorted alphabetically", + query: `json_prolog('{"b": "a", "a": "b"}', Term).`, + wantResult: []types.TermResults{{ + "Term": "json([a-b,b-a])", }}, wantSuccess: true, }, From a60f3324c587826a5c470b6b985c527aed3474a8 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Wed, 26 Apr 2023 17:10:26 +0200 Subject: [PATCH 04/20] feat(logic): json_prolog/2 handle integer number --- x/logic/predicate/json.go | 18 +++++++++++++++++- x/logic/predicate/json_test.go | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index bd3af008..245f281f 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -4,7 +4,9 @@ import ( "encoding/json" "fmt" "sort" + "strings" + "cosmossdk.io/math" "github.com/ichiban/prolog/engine" "github.com/okp4/okp4d/x/logic/util" "github.com/samber/lo" @@ -39,7 +41,12 @@ func JsonProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin func jsonStringToTerms(j string) (engine.Term, error) { var values any - json.Unmarshal([]byte(j), &values) + decoder := json.NewDecoder(strings.NewReader(j)) + decoder.UseNumber() // unmarshal a number into an interface{} as a Number instead of as a float64 + + if err := decoder.Decode(&values); err != nil { + return nil, err + } return jsonToTerms(values) } @@ -48,6 +55,15 @@ func jsonToTerms(value any) (engine.Term, error) { switch v := value.(type) { case string: return util.StringToTerm(v), nil + case json.Number: + r, ok := math.NewIntFromString(string(v)) + if !ok { + return nil, fmt.Errorf("could not convert number '%s' into integer term, decimal number is not handled yet", v) + } + if !r.IsInt64() { + return nil, fmt.Errorf("could not convert number '%s' into integer term, overflow", v) + } + return engine.Integer(r.Int64()), nil case map[string]any: keys := lo.Keys(v) sort.Strings(keys) diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 39343a73..7e03cd84 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -78,6 +78,28 @@ func TestJsonProlog(t *testing.T) { }}, wantSuccess: true, }, + // ** JSON -> Prolog ** + // Number + { + description: "convert json number into prolog", + query: `json_prolog('10', Term).`, + wantResult: []types.TermResults{{ + "Term": "10", + }}, + wantSuccess: true, + }, + { + description: "convert large json number into prolog", + query: `json_prolog('100000000000000000000', Term).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: could not convert number '100000000000000000000' into integer term, overflow"), + }, + { + description: "decimal number not compatible yet", + query: `json_prolog('10.4', Term).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: could not convert number '10.4' into integer term, decimal number is not handled yet"), + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { From 7679f94b132e251170b8c245d5d8b4bb87cd2438 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Wed, 26 Apr 2023 17:32:27 +0200 Subject: [PATCH 05/20] feat(logic): json_prolog/2 handle boolean --- x/logic/predicate/json.go | 2 ++ x/logic/predicate/json_test.go | 18 ++++++++++++++++++ x/logic/predicate/util.go | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 245f281f..194aaec4 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -64,6 +64,8 @@ func jsonToTerms(value any) (engine.Term, error) { return nil, fmt.Errorf("could not convert number '%s' into integer term, overflow", v) } return engine.Integer(r.Int64()), nil + case bool: + return AtomBool(v), nil case map[string]any: keys := lo.Keys(v) sort.Strings(keys) diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 7e03cd84..c0633b1a 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -100,6 +100,24 @@ func TestJsonProlog(t *testing.T) { wantSuccess: false, wantError: fmt.Errorf("json_prolog/2: could not convert number '10.4' into integer term, decimal number is not handled yet"), }, + // ** JSON -> Prolog ** + // Bool + { + description: "convert json true boolean into prolog", + query: `json_prolog('true', Term).`, + wantResult: []types.TermResults{{ + "Term": "@(true)", + }}, + wantSuccess: true, + }, + { + description: "convert json false boolean into prolog", + query: `json_prolog('false', Term).`, + wantResult: []types.TermResults{{ + "Term": "@(false)", + }}, + wantSuccess: true, + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { diff --git a/x/logic/predicate/util.go b/x/logic/predicate/util.go index eaab5591..e8690926 100644 --- a/x/logic/predicate/util.go +++ b/x/logic/predicate/util.go @@ -75,3 +75,13 @@ func ListToBytes(terms engine.ListIterator, env *engine.Env) ([]byte, error) { } return bt, nil } + +func AtomBool(b bool) engine.Term { + var r engine.Atom + if b { + r = engine.NewAtom("true") + } else { + r = engine.NewAtom("false") + } + return engine.NewAtom("@").Apply(r) +} From 94e9c5b85fbae0c16383d379c8e174b06be28c62 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Wed, 26 Apr 2023 17:37:11 +0200 Subject: [PATCH 06/20] feat(logic): json_prolog/2 handle null json value --- x/logic/predicate/json.go | 2 ++ x/logic/predicate/json_test.go | 10 ++++++++++ x/logic/predicate/util.go | 2 ++ 3 files changed, 14 insertions(+) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 194aaec4..a92ef3fe 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -66,6 +66,8 @@ func jsonToTerms(value any) (engine.Term, error) { return engine.Integer(r.Int64()), nil case bool: return AtomBool(v), nil + case nil: + return AtomNull, nil case map[string]any: keys := lo.Keys(v) sort.Strings(keys) diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index c0633b1a..f7529d6b 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -118,6 +118,16 @@ func TestJsonProlog(t *testing.T) { }}, wantSuccess: true, }, + // ** JSON -> Prolog ** + // Null + { + description: "convert json null value into prolog", + query: `json_prolog('null', Term).`, + wantResult: []types.TermResults{{ + "Term": "@(null)", + }}, + wantSuccess: true, + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { diff --git a/x/logic/predicate/util.go b/x/logic/predicate/util.go index e8690926..e8835e04 100644 --- a/x/logic/predicate/util.go +++ b/x/logic/predicate/util.go @@ -85,3 +85,5 @@ func AtomBool(b bool) engine.Term { } return engine.NewAtom("@").Apply(r) } + +var AtomNull = engine.NewAtom("@").Apply(engine.NewAtom("null")) From ff1f2481941c4681cd0e98e1d197b23823520d69 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Thu, 27 Apr 2023 08:26:49 +0200 Subject: [PATCH 07/20] feat(logic): json_prolog/2 handle json array --- x/logic/predicate/json.go | 10 ++++++++++ x/logic/predicate/json_test.go | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index a92ef3fe..f03343a8 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -82,6 +82,16 @@ func jsonToTerms(value any) (engine.Term, error) { } return AtomJSON.Apply(engine.List(attributes...)), nil + case []any: + elements := make([]engine.Term, 0, len(v)) + for _, element := range v { + term, err := jsonToTerms(element) + if err != nil { + return nil, err + } + elements = append(elements, term) + } + return engine.List(elements...), nil default: return nil, fmt.Errorf("could not convert %s (%T) to a prolog term", v, v) } diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index f7529d6b..dd4ac4cf 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -128,6 +128,24 @@ func TestJsonProlog(t *testing.T) { }}, wantSuccess: true, }, + // ** JSON -> Prolog ** + // Array + { + description: "convert json array into prolog", + query: `json_prolog('["foo", "bar"]', Term).`, + wantResult: []types.TermResults{{ + "Term": "[foo,bar]", + }}, + wantSuccess: true, + }, + { + description: "convert json string array into prolog", + query: `json_prolog('["string with space", "bar"]', Term).`, + wantResult: []types.TermResults{{ + "Term": "['string with space',bar]", + }}, + wantSuccess: true, + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { From c0b5a6c5290f3aedbbd0b9f7d9f7a712f8d773f7 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Thu, 27 Apr 2023 10:01:23 +0200 Subject: [PATCH 08/20] feat(logic): json_prolog/2 handle string term to json string --- x/logic/predicate/json.go | 23 +++++++++++++++++++---- x/logic/predicate/json_test.go | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index f03343a8..4485162f 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -30,12 +30,18 @@ func JsonProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin return engine.Unify(vm, term, terms, cont, env) default: - return engine.Error(fmt.Errorf("did_components/2: cannot unify json with %T", t1)) + return engine.Error(fmt.Errorf("json_prolog/2: cannot unify json with %T", t1)) } - switch env.Resolve(term).(type) { + switch t2 := env.Resolve(term).(type) { + case engine.Variable: + return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) default: - return engine.Error(fmt.Errorf("json_prolog/2: not implemented")) + b, err := termsToJson(t2) + if err != nil { + return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + } + return engine.Unify(vm, j, util.StringToTerm(string(b)), cont, env) } } @@ -51,6 +57,16 @@ func jsonStringToTerms(j string) (engine.Term, error) { return jsonToTerms(values) } +func termsToJson(term engine.Term) ([]byte, error) { + switch t := term.(type) { + case engine.Atom: + return json.Marshal(t.String()) + default: + return nil, fmt.Errorf("could not convert %s {%T} to json", t, t) + } + +} + func jsonToTerms(value any) (engine.Term, error) { switch v := value.(type) { case string: @@ -80,7 +96,6 @@ func jsonToTerms(value any) (engine.Term, error) { } attributes = append(attributes, AtomPair.Apply(engine.NewAtom(key), attributeValue)) } - return AtomJSON.Apply(engine.List(attributes...)), nil case []any: elements := make([]engine.Term, 0, len(v)) diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index dd4ac4cf..7a799a57 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -146,6 +146,25 @@ func TestJsonProlog(t *testing.T) { }}, wantSuccess: true, }, + + // ** Prolog -> JSON ** + // String + { + description: "convert string term to json", + query: `json_prolog(Json, 'foo').`, + wantResult: []types.TermResults{{ + "Json": "'\"foo\"'", + }}, + wantSuccess: true, + }, + { + description: "convert string with space to json", + query: `json_prolog(Json, 'foo bar').`, + wantResult: []types.TermResults{{ + "Json": "'\"foo bar\"'", + }}, + wantSuccess: true, + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { From 9b58497948ba6d4d687fb80269bb5e03edef3550 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 28 Apr 2023 10:40:10 +0200 Subject: [PATCH 09/20] feat(logic): add util func to extract json object attribute --- x/logic/predicate/util.go | 46 +++++++++++++++++++ x/logic/predicate/util_test.go | 80 ++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 x/logic/predicate/util_test.go 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) + }) + }) + } + }) + }) + } + }) +} From 4bb3f9a0ad6825af1033ffed9888710efd353951 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 28 Apr 2023 10:40:44 +0200 Subject: [PATCH 10/20] feat(logic): json_prolog/2 handle json term to json object --- x/logic/predicate/json.go | 26 +++++++++++++++++++--- x/logic/predicate/json_test.go | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 4485162f..dbf946d2 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -7,6 +7,7 @@ import ( "strings" "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ichiban/prolog/engine" "github.com/okp4/okp4d/x/logic/util" "github.com/samber/lo" @@ -37,7 +38,12 @@ func JsonProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin case engine.Variable: return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) default: - b, err := termsToJson(t2) + b, err := termsToJson(t2, env) + if err != nil { + return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + } + + b, err = sdk.SortJSON(b) if err != nil { return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) } @@ -57,14 +63,28 @@ func jsonStringToTerms(j string) (engine.Term, error) { return jsonToTerms(values) } -func termsToJson(term engine.Term) ([]byte, error) { +func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { switch t := term.(type) { case engine.Atom: return json.Marshal(t.String()) + case engine.Compound: + terms, err := ExtractJsonTerm(t, env) + if err != nil { + return nil, err + } + + attributes := make(map[string]json.RawMessage, len(terms)) + for key, term := range terms { + raw, err := termsToJson(env.Resolve(term), env) + if err != nil { + return nil, err + } + attributes[key] = raw + } + return json.Marshal(attributes) default: return nil, fmt.Errorf("could not convert %s {%T} to json", t, t) } - } func jsonToTerms(value any) (engine.Term, error) { diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 7a799a57..084be78b 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -165,6 +165,46 @@ func TestJsonProlog(t *testing.T) { }}, wantSuccess: true, }, + // ** Prolog -> JSON ** + // Object + { + description: "convert json object from prolog", + query: `json_prolog(Json, json([foo-bar])).`, + wantResult: []types.TermResults{{ + "Json": "'{\"foo\":\"bar\"}'", + }}, + wantSuccess: true, + }, + { + description: "convert json object with multiple attribute from prolog", + query: `json_prolog(Json, json([foo-bar,foobar-'bar foo'])).`, + wantResult: []types.TermResults{{ + "Json": "'{\"foo\":\"bar\",\"foobar\":\"bar foo\"}'", + }}, + wantSuccess: true, + }, + { + description: "convert json object with attribute with a space into prolog", + query: `json_prolog(Json, json(['string with space'-bar])).`, + wantResult: []types.TermResults{{ + "Json": "'{\"string with space\":\"bar\"}'", + }}, + wantSuccess: true, + }, + { + description: "ensure determinism on object attribute key sorted alphabetically", + query: `json_prolog(Json, json([b-a,a-b])).`, + wantResult: []types.TermResults{{ + "Json": "'{\"a\":\"b\",\"b\":\"a\"}'", + }}, + wantSuccess: true, + }, + { + description: "invalid json term compound", + query: `json_prolog(Json, foo([a-b])).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: invalid functor foo. Expected json"), + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { From 4e2b8b6bf590b03c1defc930d7d6082832c0b3d5 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 28 Apr 2023 11:16:48 +0200 Subject: [PATCH 11/20] feat(logic): json_prolog/2 handle list term to json array --- x/logic/predicate/json.go | 36 +++++++++++++++++++++++++--------- x/logic/predicate/json_test.go | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index dbf946d2..7226843e 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -67,21 +67,39 @@ func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { switch t := term.(type) { case engine.Atom: return json.Marshal(t.String()) + case engine.Integer: + return json.Marshal(t) case engine.Compound: - terms, err := ExtractJsonTerm(t, env) - if err != nil { - return nil, err - } + if t.Arity() == 2 && t.Functor().String() == "." { + // It's engine.List + iter := engine.ListIterator{List: t, Env: env} - attributes := make(map[string]json.RawMessage, len(terms)) - for key, term := range terms { - raw, err := termsToJson(env.Resolve(term), env) + elements := make([]json.RawMessage, 0) + for iter.Next() { + element, err := termsToJson(env.Resolve(iter.Current()), env) + if err != nil { + return nil, err + } + elements = append(elements, element) + } + return json.Marshal(elements) + } else { + // It's a json atom + terms, err := ExtractJsonTerm(t, env) if err != nil { return nil, err } - attributes[key] = raw + + attributes := make(map[string]json.RawMessage, len(terms)) + for key, term := range terms { + raw, err := termsToJson(env.Resolve(term), env) + if err != nil { + return nil, err + } + attributes[key] = raw + } + return json.Marshal(attributes) } - return json.Marshal(attributes) default: return nil, fmt.Errorf("could not convert %s {%T} to json", t, t) } diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 084be78b..bf6cc95a 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -205,6 +205,40 @@ func TestJsonProlog(t *testing.T) { wantSuccess: false, wantError: fmt.Errorf("json_prolog/2: invalid functor foo. Expected json"), }, + // ** Prolog -> JSON ** + // Number + { + description: "convert json number from prolog", + query: `json_prolog(Json, 10).`, + wantResult: []types.TermResults{{ + "Json": "'10'", + }}, + wantSuccess: true, + }, + { + description: "decimal number not compatible yet", + query: `json_prolog(Json, 10.4).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: could not convert %%!s(engine.Float=10.4) {engine.Float} to json"), + }, + // ** Prolog -> Json ** + // Array + { + description: "convert json array from prolog", + query: `json_prolog(Json, [foo,bar]).`, + wantResult: []types.TermResults{{ + "Json": "'[\"foo\",\"bar\"]'", + }}, + wantSuccess: true, + }, + { + description: "convert json string array from prolog", + query: `json_prolog(Json, ['string with space',bar]).`, + wantResult: []types.TermResults{{ + "Json": "'[\"string with space\",\"bar\"]'", + }}, + wantSuccess: true, + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { From 9c3b7f8364e0ca8e7552b821873d76e3eefcfec0 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 28 Apr 2023 14:45:15 +0200 Subject: [PATCH 12/20] feat(logic): json_prolog/2 handle boolean and null --- x/logic/predicate/json.go | 21 ++++++++++++++++++--- x/logic/predicate/json_test.go | 30 +++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 7226843e..2ceb2526 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -70,8 +70,12 @@ func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { case engine.Integer: return json.Marshal(t) case engine.Compound: - if t.Arity() == 2 && t.Functor().String() == "." { - // It's engine.List + switch t.Functor().String() { + case ".": // Represent an engine.List + if t.Arity() != 2 { + return nil, fmt.Errorf("wrong term arity for array, give %d, expected %d", t.Arity(), 2) + } + iter := engine.ListIterator{List: t, Env: env} elements := make([]json.RawMessage, 0) @@ -83,7 +87,7 @@ func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { elements = append(elements, element) } return json.Marshal(elements) - } else { + case AtomJSON.String(): // It's a json atom terms, err := ExtractJsonTerm(t, env) if err != nil { @@ -100,6 +104,17 @@ func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { } return json.Marshal(attributes) } + + if AtomBool(true).Compare(t, env) == 0 { + return json.Marshal(true) + } else if AtomBool(false).Compare(t, env) == 0 { + return json.Marshal(false) + } else if AtomNull.Compare(t, env) == 0 { + return json.Marshal(nil) + } + + return nil, fmt.Errorf("invalid functor %s", t.Functor()) + default: return nil, fmt.Errorf("could not convert %s {%T} to json", t, t) } diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index bf6cc95a..fa675c6a 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -203,7 +203,7 @@ func TestJsonProlog(t *testing.T) { description: "invalid json term compound", query: `json_prolog(Json, foo([a-b])).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: invalid functor foo. Expected json"), + wantError: fmt.Errorf("json_prolog/2: invalid functor foo"), }, // ** Prolog -> JSON ** // Number @@ -239,6 +239,34 @@ func TestJsonProlog(t *testing.T) { }}, wantSuccess: true, }, + // ** Prolog -> JSON ** + // Bool + { + description: "convert true boolean from prolog", + query: `json_prolog(Json, @(true)).`, + wantResult: []types.TermResults{{ + "Json": "true", + }}, + wantSuccess: true, + }, + { + description: "convert false boolean from prolog", + query: `json_prolog(Json, @(false)).`, + wantResult: []types.TermResults{{ + "Json": "false", + }}, + wantSuccess: true, + }, + // ** Prolog -> Json ** + // Null + { + description: "convert json null value into prolog", + query: `json_prolog(Json, @(null)).`, + wantResult: []types.TermResults{{ + "Json": "null", + }}, + wantSuccess: true, + }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { From 0307ab59a84b6101e8b68b0cc91a21ff1989227c Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 28 Apr 2023 15:48:41 +0200 Subject: [PATCH 13/20] test(logic): add bidirectinnal test for json_prolog/2 --- x/logic/predicate/json_test.go | 122 +++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index fa675c6a..749c2ddc 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -325,3 +325,125 @@ func TestJsonProlog(t *testing.T) { } }) } + +func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { + Convey("Given a test cases", t, func() { + cases := []struct { + json string + term string + wantError error + wantSuccess bool + }{ + { + json: "'{\"foo\":\"bar\"}'", + term: "json([foo-bar])", + wantSuccess: true, + }, + { + json: "'{\"employee\":{\"age\":30,\"city\":\"New York\",\"name\":\"John\"}}'", + term: "json([employee-json([age-30,city-'New York',name-'John'])])", + wantSuccess: true, + }, + { + json: "'{\"cosmos\":[\"okp4\",{\"name\":\"localnet\"}]}'", + term: "json([cosmos-[okp4,json([name-localnet])]])", + wantSuccess: true, + }, + { + json: "'{\"object\":{\"array\":[1,2,3],\"arrayobject\":[{\"name\":\"toto\"},{\"name\":\"tata\"}],\"bool\":true,\"boolean\":false,\"null\":null}}'", + term: "json([object-json([array-[1,2,3],arrayobject-[json([name-toto]),json([name-tata])],bool- @(true),boolean- @(false),null- @(null)])])", + wantSuccess: true, + }, + } + for nc, tc := range cases { + Convey(fmt.Sprintf("#%d : given the json: %s and the term %s", nc, tc.json, tc.term), func() { + Convey("and a context", func() { + db := tmdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db) + ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) + + Convey("and a vm", func() { + interpreter := testutil.NewLightInterpreterMust(ctx) + interpreter.Register2(engine.NewAtom("json_prolog"), JsonProlog) + + Convey("When the predicate `json_prolog` is called to convert json to prolog", func() { + sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(%s, Term).", tc.json)) + + Convey("Then the error should be nil", func() { + So(err, ShouldBeNil) + So(sols, ShouldNotBeNil) + + Convey("and the bindings should be as expected", func() { + var got []types.TermResults + for sols.Next() { + m := types.TermResults{} + err := sols.Scan(m) + So(err, ShouldBeNil) + + got = append(got, m) + } + if tc.wantError != nil { + So(sols.Err(), ShouldNotBeNil) + So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) + } else { + So(sols.Err(), ShouldBeNil) + + if tc.wantSuccess { + So(len(got), ShouldBeGreaterThan, 0) + So(len(got), ShouldEqual, 1) + for _, resultGot := range got { + for _, termGot := range resultGot { + So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.term) + } + } + } else { + So(len(got), ShouldEqual, 0) + } + } + }) + }) + }) + + Convey("When the predicate `json_prolog` is called to convert prolog to json", func() { + sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(Json, %s).", tc.term)) + + Convey("Then the error should be nil", func() { + So(err, ShouldBeNil) + So(sols, ShouldNotBeNil) + + Convey("and the bindings should be as expected", func() { + var got []types.TermResults + for sols.Next() { + m := types.TermResults{} + err := sols.Scan(m) + So(err, ShouldBeNil) + + got = append(got, m) + } + if tc.wantError != nil { + So(sols.Err(), ShouldNotBeNil) + So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) + } else { + So(sols.Err(), ShouldBeNil) + + if tc.wantSuccess { + So(len(got), ShouldBeGreaterThan, 0) + So(len(got), ShouldEqual, 1) + for _, resultGot := range got { + for _, termGot := range resultGot { + So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.json) + } + } + } else { + So(len(got), ShouldEqual, 0) + } + } + }) + }) + }) + }) + }) + }) + } + }) +} From 5b85987f102fb8efe57cf70c442d46d62fff1484 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 28 Apr 2023 15:58:19 +0200 Subject: [PATCH 14/20] feat(logic): include json_prolog/2 into the registry --- x/logic/interpreter/registry.go | 1 + 1 file changed, 1 insertion(+) diff --git a/x/logic/interpreter/registry.go b/x/logic/interpreter/registry.go index 7935cbcc..386b5967 100644 --- a/x/logic/interpreter/registry.go +++ b/x/logic/interpreter/registry.go @@ -111,6 +111,7 @@ var registry = map[string]any{ "hex_bytes/2": predicate.HexBytes, "bech32_address/2": predicate.Bech32Address, "source_file/1": predicate.SourceFile, + "json_prolog/2": predicate.JsonProlog, } // RegistryNames is the list of the predicate names in the Registry. From 62df94ef9dddf87a4096e8bf14634c5a5a7d5676 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 28 Apr 2023 16:40:11 +0200 Subject: [PATCH 15/20] style: refactor for linter --- x/logic/interpreter/registry.go | 2 +- x/logic/predicate/json.go | 22 +++++++++++----------- x/logic/predicate/json_test.go | 4 ++-- x/logic/predicate/util.go | 4 ++-- x/logic/predicate/util_test.go | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/x/logic/interpreter/registry.go b/x/logic/interpreter/registry.go index 386b5967..2ba74b70 100644 --- a/x/logic/interpreter/registry.go +++ b/x/logic/interpreter/registry.go @@ -111,7 +111,7 @@ var registry = map[string]any{ "hex_bytes/2": predicate.HexBytes, "bech32_address/2": predicate.Bech32Address, "source_file/1": predicate.SourceFile, - "json_prolog/2": predicate.JsonProlog, + "json_prolog/2": predicate.JSONProlog, } // RegistryNames is the list of the predicate names in the Registry. diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 2ceb2526..ebc07a66 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -16,11 +16,11 @@ import ( // AtomJSON is a term which represents a json as a compound term `json([Pair])`. var AtomJSON = engine.NewAtom("json") -// JsonProlog is a predicate that will unify a JSON string into prolog terms and vice versa. +// JSONProlog is a predicate that will unify a JSON string into prolog terms and vice versa. // // json_prolog(?Json, ?Term) is det // TODO: -func JsonProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { +func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { switch t1 := env.Resolve(j).(type) { case engine.Variable: case engine.Atom: @@ -38,7 +38,7 @@ func JsonProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin case engine.Variable: return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) default: - b, err := termsToJson(t2, env) + b, err := termsToJSON(t2, env) if err != nil { return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) } @@ -63,7 +63,7 @@ func jsonStringToTerms(j string) (engine.Term, error) { return jsonToTerms(values) } -func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { +func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) { switch t := term.(type) { case engine.Atom: return json.Marshal(t.String()) @@ -80,7 +80,7 @@ func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { elements := make([]json.RawMessage, 0) for iter.Next() { - element, err := termsToJson(env.Resolve(iter.Current()), env) + element, err := termsToJSON(env.Resolve(iter.Current()), env) if err != nil { return nil, err } @@ -89,14 +89,14 @@ func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { return json.Marshal(elements) case AtomJSON.String(): // It's a json atom - terms, err := ExtractJsonTerm(t, env) + terms, err := ExtractJSONTerm(t, env) if err != nil { return nil, err } attributes := make(map[string]json.RawMessage, len(terms)) for key, term := range terms { - raw, err := termsToJson(env.Resolve(term), env) + raw, err := termsToJSON(env.Resolve(term), env) if err != nil { return nil, err } @@ -105,16 +105,16 @@ func termsToJson(term engine.Term, env *engine.Env) ([]byte, error) { return json.Marshal(attributes) } - if AtomBool(true).Compare(t, env) == 0 { + switch { + case AtomBool(true).Compare(t, env) == 0: return json.Marshal(true) - } else if AtomBool(false).Compare(t, env) == 0 { + case AtomBool(false).Compare(t, env) == 0: return json.Marshal(false) - } else if AtomNull.Compare(t, env) == 0 { + case AtomNull.Compare(t, env) == 0: return json.Marshal(nil) } return nil, fmt.Errorf("invalid functor %s", t.Functor()) - default: return nil, fmt.Errorf("could not convert %s {%T} to json", t, t) } diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 749c2ddc..fe37cea1 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -277,7 +277,7 @@ func TestJsonProlog(t *testing.T) { Convey("and a vm", func() { interpreter := testutil.NewLightInterpreterMust(ctx) - interpreter.Register2(engine.NewAtom("json_prolog"), JsonProlog) + interpreter.Register2(engine.NewAtom("json_prolog"), JSONProlog) err := interpreter.Compile(ctx, tc.program) So(err, ShouldBeNil) @@ -364,7 +364,7 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { Convey("and a vm", func() { interpreter := testutil.NewLightInterpreterMust(ctx) - interpreter.Register2(engine.NewAtom("json_prolog"), JsonProlog) + interpreter.Register2(engine.NewAtom("json_prolog"), JSONProlog) Convey("When the predicate `json_prolog` is called to convert json to prolog", func() { sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(%s, Term).", tc.json)) diff --git a/x/logic/predicate/util.go b/x/logic/predicate/util.go index 97908eea..415397df 100644 --- a/x/logic/predicate/util.go +++ b/x/logic/predicate/util.go @@ -88,7 +88,7 @@ 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 +// 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. @@ -101,7 +101,7 @@ var AtomNull = engine.NewAtom("@").Apply(engine.NewAtom("null")) // // 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) { +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 { diff --git a/x/logic/predicate/util_test.go b/x/logic/predicate/util_test.go index 4d9b8a0f..598bd5ea 100644 --- a/x/logic/predicate/util_test.go +++ b/x/logic/predicate/util_test.go @@ -53,7 +53,7 @@ func TestExtractJsonTerm(t *testing.T) { 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) + result, err := ExtractJSONTerm(tc.compound, &env) if tc.wantSuccess { Convey("then no error should be thrown", func() { From 356c15b77416a625857681781539b2ceb57ff98a Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Mon, 1 May 2023 22:04:39 +0200 Subject: [PATCH 16/20] docs: add docs on jsonProlog func --- x/logic/predicate/json.go | 22 ++++++++++++++++++---- x/logic/predicate/json_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index ebc07a66..f3a8a99c 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -19,8 +19,20 @@ var AtomJSON = engine.NewAtom("json") // JSONProlog is a predicate that will unify a JSON string into prolog terms and vice versa. // // json_prolog(?Json, ?Term) is det -// TODO: +// +// Where +// - `Json` is the string representation of the json +// - `Term` is an Atom that would be unified by the JSON representation as Prolog terms. +// +// In addition, when passing Json and Term, this predicate return true if both result match. +// +// Example: +// +// # JSON conversion to Prolog. +// - json_prolog('{"foo": "bar"}', json([foo-bar])). func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { + var result engine.Term + switch t1 := env.Resolve(j).(type) { case engine.Variable: case engine.Atom: @@ -28,15 +40,17 @@ func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin if err != nil { return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) } - - return engine.Unify(vm, term, terms, cont, env) + result = terms default: return engine.Error(fmt.Errorf("json_prolog/2: cannot unify json with %T", t1)) } switch t2 := env.Resolve(term).(type) { case engine.Variable: - return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) + if result == nil { + return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) + } + return engine.Unify(vm, term, result, cont, env) default: b, err := termsToJSON(t2, env) if err != nil { diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index fe37cea1..7f657e22 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -26,6 +26,19 @@ func TestJsonProlog(t *testing.T) { wantError error wantSuccess bool }{ + { + description: "two variable", + query: `json_prolog(Json, Term).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: could not unify two variable"), + }, + { + description: "two variable", + query: `json_prolog(ooo(r), Term).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: cannot unify json with *engine.compound"), + }, + // ** JSON -> Prolog ** // String { @@ -205,6 +218,18 @@ func TestJsonProlog(t *testing.T) { wantSuccess: false, wantError: fmt.Errorf("json_prolog/2: invalid functor foo"), }, + { + description: "convert json term object from prolog with error inside", + query: `json_prolog(Json, ['string with space',json('toto')]).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: json compound should contains one list, give engine.Atom"), + }, + { + description: "convert json term object from prolog with error inside another object", + query: `json_prolog(Json, ['string with space',json([key-json(error)])]).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: json compound should contains one list, give engine.Atom"), + }, // ** Prolog -> JSON ** // Number { @@ -239,6 +264,12 @@ func TestJsonProlog(t *testing.T) { }}, wantSuccess: true, }, + { + description: "convert json string array from prolog with error inside", + query: `json_prolog(Json, ['string with space',hey('toto')]).`, + wantSuccess: false, + wantError: fmt.Errorf("json_prolog/2: invalid functor hey"), + }, // ** Prolog -> JSON ** // Bool { From 3e01dfaf6a6e51f480142d332516833183798303 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Mon, 1 May 2023 22:25:39 +0200 Subject: [PATCH 17/20] test(logic): add test for json_prolog matching checking --- x/logic/predicate/json_test.go | 117 +++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 7f657e22..b7c88dd6 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -385,6 +385,11 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { term: "json([object-json([array-[1,2,3],arrayobject-[json([name-toto]),json([name-tata])],bool- @(true),boolean- @(false),null- @(null)])])", wantSuccess: true, }, + { + json: "'{\"foo\":\"bar\"}'", + term: "json([a-b])", + wantSuccess: false, + }, } for nc, tc := range cases { Convey(fmt.Sprintf("#%d : given the json: %s and the term %s", nc, tc.json, tc.term), func() { @@ -397,46 +402,86 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { interpreter := testutil.NewLightInterpreterMust(ctx) interpreter.Register2(engine.NewAtom("json_prolog"), JSONProlog) - Convey("When the predicate `json_prolog` is called to convert json to prolog", func() { - sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(%s, Term).", tc.json)) + if tc.wantSuccess { + Convey("When the predicate `json_prolog` is called to convert json to prolog", func() { + sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(%s, Term).", tc.json)) - Convey("Then the error should be nil", func() { - So(err, ShouldBeNil) - So(sols, ShouldNotBeNil) + Convey("Then the error should be nil", func() { + So(err, ShouldBeNil) + So(sols, ShouldNotBeNil) - Convey("and the bindings should be as expected", func() { - var got []types.TermResults - for sols.Next() { - m := types.TermResults{} - err := sols.Scan(m) - So(err, ShouldBeNil) + Convey("and the bindings should be as expected", func() { + var got []types.TermResults + for sols.Next() { + m := types.TermResults{} + err := sols.Scan(m) + So(err, ShouldBeNil) - got = append(got, m) - } - if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) - So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) - } else { - So(sols.Err(), ShouldBeNil) + got = append(got, m) + } + if tc.wantError != nil { + So(sols.Err(), ShouldNotBeNil) + So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) + } else { + So(sols.Err(), ShouldBeNil) - if tc.wantSuccess { - So(len(got), ShouldBeGreaterThan, 0) - So(len(got), ShouldEqual, 1) - for _, resultGot := range got { - for _, termGot := range resultGot { - So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.term) + if tc.wantSuccess { + So(len(got), ShouldBeGreaterThan, 0) + So(len(got), ShouldEqual, 1) + for _, resultGot := range got { + for _, termGot := range resultGot { + So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.term) + } } + } else { + So(len(got), ShouldEqual, 0) } + } + }) + }) + }) + + Convey("When the predicate `json_prolog` is called to convert prolog to json", func() { + sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(Json, %s).", tc.term)) + + Convey("Then the error should be nil", func() { + So(err, ShouldBeNil) + So(sols, ShouldNotBeNil) + + Convey("and the bindings should be as expected", func() { + var got []types.TermResults + for sols.Next() { + m := types.TermResults{} + err := sols.Scan(m) + So(err, ShouldBeNil) + + got = append(got, m) + } + if tc.wantError != nil { + So(sols.Err(), ShouldNotBeNil) + So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(len(got), ShouldEqual, 0) + So(sols.Err(), ShouldBeNil) + + if tc.wantSuccess { + So(len(got), ShouldBeGreaterThan, 0) + So(len(got), ShouldEqual, 1) + for _, resultGot := range got { + for _, termGot := range resultGot { + So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.json) + } + } + } else { + So(len(got), ShouldEqual, 0) + } } - } + }) }) }) - }) + } - Convey("When the predicate `json_prolog` is called to convert prolog to json", func() { - sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(Json, %s).", tc.term)) + Convey("When the predicate `json_prolog` is called to check prolog matching json", func() { + sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(%s, %s).", tc.json, tc.term)) Convey("Then the error should be nil", func() { So(err, ShouldBeNil) @@ -454,20 +499,6 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { if tc.wantError != nil { So(sols.Err(), ShouldNotBeNil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) - } else { - So(sols.Err(), ShouldBeNil) - - if tc.wantSuccess { - So(len(got), ShouldBeGreaterThan, 0) - So(len(got), ShouldEqual, 1) - for _, resultGot := range got { - for _, termGot := range resultGot { - So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.json) - } - } - } else { - So(len(got), ShouldEqual, 0) - } } }) }) From 3ac7399fcf0243a50a6dc4c51738a02123233c74 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Mon, 1 May 2023 22:34:09 +0200 Subject: [PATCH 18/20] ci: fix dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 50deba00..4ee680ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ ADD https://github.com/CosmWasm/wasmvm/releases/download/v1.2.1/libwasmvm_muslc. # hadolint ignore=DL4006 RUN set -eux \ - && apk add --no-cache ca-certificates=20220614-r0 build-base=0.5-r3 git=2.36.5-r0 linux-headers=5.16.7-r1 \ + && apk add --no-cache ca-certificates=20220614-r0 build-base=0.5-r3 git=2.36.6-r0 linux-headers=5.16.7-r1 \ && sha256sum /lib/libwasmvm_muslc.aarch64.a | grep 86bc5fdc0f01201481c36e17cd3dfed6e9650d22e1c5c8983a5b78c231789ee0 \ && sha256sum /lib/libwasmvm_muslc.x86_64.a | grep a00700aa19f5bfe0f46290ddf69bf51eb03a6dfcd88b905e1081af2e42dbbafc \ && cp "/lib/libwasmvm_muslc.$(uname -m).a" /lib/libwasmvm_muslc.a From b04b56ece4065fcd35cc7fb11d55cbae45e6da11 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Tue, 2 May 2023 09:51:49 +0200 Subject: [PATCH 19/20] style(logic): fix linter --- x/logic/predicate/json_test.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index b7c88dd6..5d8f7c4a 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -1,4 +1,4 @@ -//nolint:gocognit,lll +//nolint:gocognit,lll,nestif package predicate import ( @@ -488,14 +488,8 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { - var got []types.TermResults - for sols.Next() { - m := types.TermResults{} - err := sols.Scan(m) - So(err, ShouldBeNil) + So(sols.Next(), ShouldEqual, tc.wantSuccess) - got = append(got, m) - } if tc.wantError != nil { So(sols.Err(), ShouldNotBeNil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) From e41ea0345eaec04eb3dd543a24b827bd42ac282e Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Tue, 2 May 2023 15:29:56 +0200 Subject: [PATCH 20/20] style(logic): clean some comment and tests --- x/logic/predicate/json.go | 58 ++++++++++++++++++---------------- x/logic/predicate/json_test.go | 10 ++++++ 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index f3a8a99c..3fbffc07 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -1,6 +1,7 @@ package predicate import ( + "context" "encoding/json" "fmt" "sort" @@ -31,38 +32,40 @@ var AtomJSON = engine.NewAtom("json") // # JSON conversion to Prolog. // - json_prolog('{"foo": "bar"}', json([foo-bar])). func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - var result engine.Term + return engine.Delay(func(ctx context.Context) *engine.Promise { + var result engine.Term - switch t1 := env.Resolve(j).(type) { - case engine.Variable: - case engine.Atom: - terms, err := jsonStringToTerms(t1.String()) - if err != nil { - return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + switch t1 := env.Resolve(j).(type) { + case engine.Variable: + case engine.Atom: + terms, err := jsonStringToTerms(t1.String()) + if err != nil { + return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + } + result = terms + default: + return engine.Error(fmt.Errorf("json_prolog/2: cannot unify json with %T", t1)) } - result = terms - default: - return engine.Error(fmt.Errorf("json_prolog/2: cannot unify json with %T", t1)) - } - switch t2 := env.Resolve(term).(type) { - case engine.Variable: - if result == nil { - return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) - } - return engine.Unify(vm, term, result, cont, env) - default: - b, err := termsToJSON(t2, env) - if err != nil { - return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) - } + switch t2 := env.Resolve(term).(type) { + case engine.Variable: + if result == nil { + return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) + } + return engine.Unify(vm, term, result, cont, env) + default: + b, err := termsToJSON(t2, env) + if err != nil { + return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + } - b, err = sdk.SortJSON(b) - if err != nil { - return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + b, err = sdk.SortJSON(b) + if err != nil { + return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + } + return engine.Unify(vm, j, util.StringToTerm(string(b)), cont, env) } - return engine.Unify(vm, j, util.StringToTerm(string(b)), cont, env) - } + }) } func jsonStringToTerms(j string) (engine.Term, error) { @@ -102,7 +105,6 @@ func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) { } return json.Marshal(elements) case AtomJSON.String(): - // It's a json atom terms, err := ExtractJSONTerm(t, env) if err != nil { return nil, err diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 5d8f7c4a..19c13334 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -370,6 +370,16 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { term: "json([foo-bar])", wantSuccess: true, }, + { + json: "'{\"foo\":\"null\"}'", + term: "json([foo-null])", + wantSuccess: true, + }, + { + json: "'{\"foo\":null}'", + term: "json([foo- @(null)])", + wantSuccess: true, + }, { json: "'{\"employee\":{\"age\":30,\"city\":\"New York\",\"name\":\"John\"}}'", term: "json([employee-json([age-30,city-'New York',name-'John'])])",