Skip to content

Commit

Permalink
refactor(logic)!: use '=' functor for encoding key-value pairs in jso…
Browse files Browse the repository at this point in the history
…n_prolog/2

and align with the encoding convention used in SWI-Prolog.
  • Loading branch information
ccamel committed Oct 12, 2024
1 parent c3e5781 commit 37d4518
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 52 deletions.
43 changes: 18 additions & 25 deletions x/logic/predicate/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var (
// # JSON canonical representation
//
// The canonical representation for Term is:
// - A JSON object is mapped to a Prolog term json(NameValueList), where NameValueList is a list of Name-Value pairs.
// - A JSON object is mapped to a Prolog term json(NameValueList), where NameValueList is a list of Name=Value key values.
// Name is an atom created from the JSON string.
// - A JSON array is mapped to a Prolog list of JSON values.
// - A JSON string is mapped to a Prolog atom.
Expand All @@ -56,7 +56,7 @@ var (
// # Examples:
//
// # JSON conversion to Prolog.
// - json_prolog('{"foo": "bar"}', json([foo-bar])).
// - json_prolog('{"foo": "bar"}', json([foo=bar])).
func JSONProlog(_ *engine.VM, j, p engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
forwardConverter := func(in []engine.Term, _ engine.Term, env *engine.Env) ([]engine.Term, error) {
payload, err := prolog.TextTermToString(in[0], env)
Expand Down Expand Up @@ -89,31 +89,21 @@ func JSONProlog(_ *engine.VM, j, p engine.Term, cont engine.Cont, env *engine.En
}

func encodeTermToJSON(term engine.Term, buf *bytes.Buffer, env *engine.Env) (err error) {
marshalToBuffer := func(data any) error {
bs, err := json.Marshal(data)
if err != nil {
return prologErrorToException(term, err, env)
}
buf.Write(bs)

return nil
}

switch t := term.(type) {
case engine.Atom:
if term == prolog.AtomEmptyList {
buf.Write([]byte("[]"))
} else {
return marshalToBuffer(t.String())
return marshalToBuffer(t.String(), term, buf, env)
}
case engine.Integer:
return marshalToBuffer(t)
return marshalToBuffer(t, term, buf, env)
case engine.Float:
float, err := strconv.ParseFloat(t.String(), 64)
if err != nil {
return prologErrorToException(t, err, env)
}
return marshalToBuffer(float)
return marshalToBuffer(float, term, buf, env)
case engine.Compound:
return encodeCompoundToJSON(t, buf, env)
default:
Expand All @@ -123,6 +113,16 @@ func encodeTermToJSON(term engine.Term, buf *bytes.Buffer, env *engine.Env) (err
return nil
}

func marshalToBuffer(data any, term engine.Term, buf *bytes.Buffer, env *engine.Env) error {
bs, err := json.Marshal(data)
if err != nil {
return prologErrorToException(term, err, env)
}
buf.Write(bs)

return nil
}

func encodeCompoundToJSON(term engine.Compound, buf *bytes.Buffer, env *engine.Env) error {
switch {
case term.Functor() == prolog.AtomDot:
Expand All @@ -148,19 +148,13 @@ func encodeObjectToJSON(term engine.Compound, buf *bytes.Buffer, env *engine.Env
}
buf.Write([]byte("{"))
if err := prolog.ForEach(term.Arg(0), env, func(t engine.Term, hasNext bool) error {
k, v, err := prolog.AssertPair(t, env)
k, v, err := prolog.AssertKeyValue(t, env)
if err != nil {
return err
}
key, err := prolog.AssertAtom(k, env)
if err != nil {
if err := marshalToBuffer(k.String(), term, buf, env); err != nil {
return err
}
bs, err := json.Marshal(key.String())
if err != nil {
return prologErrorToException(t, err, env)
}
buf.Write(bs)
buf.Write([]byte(":"))
if err := encodeTermToJSON(v, buf, env); err != nil {
return err
Expand All @@ -174,7 +168,6 @@ func encodeObjectToJSON(term engine.Compound, buf *bytes.Buffer, env *engine.Env
return err
}
buf.Write([]byte("}"))

return nil
}

Expand Down Expand Up @@ -303,7 +296,7 @@ func decodeJSONObjectToTerm(decoder *json.Decoder, env *engine.Env) (engine.Term
if err != nil {
return nil, err
}
terms = append(terms, prolog.AtomPair.Apply(prolog.StringToAtom(key), value))
terms = append(terms, prolog.AtomKeyValue.Apply(prolog.StringToAtom(key), value))
}

return prolog.AtomJSON.Apply(engine.List(terms...)), nil
Expand Down
54 changes: 30 additions & 24 deletions x/logic/predicate/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,39 +67,39 @@ func TestJsonProlog(t *testing.T) {
description: "convert json object into prolog",
query: `json_prolog('{"foo": "bar"}', Term).`,
wantResult: []testutil.TermResults{{
"Term": "json([foo-bar])",
"Term": "json([foo=bar])",
}},
wantSuccess: true,
},
{
description: "convert json object (given as string) into prolog",
query: `json_prolog("{\"foo\": \"bar\"}", Term).`,
wantResult: []testutil.TermResults{{
"Term": "json([foo-bar])",
"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: []testutil.TermResults{{
"Term": "json([foo-bar,foobar-'bar foo'])",
"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: []testutil.TermResults{{
"Term": "json(['string with space'-bar])",
"Term": "json(['string with space'=bar])",
}},
wantSuccess: true,
},
{
description: "ensure prolog encoded json follows same order as json",
query: `json_prolog('{"b": "a", "a": "b"}', Term).`,
wantResult: []testutil.TermResults{{
"Term": "json([b-a,a-b])",
"Term": "json([b=a,a=b])",
}},
wantSuccess: true,
},
Expand Down Expand Up @@ -275,41 +275,41 @@ func TestJsonProlog(t *testing.T) {
// Object
{
description: "convert json object from prolog",
query: `json_prolog(Json, json([foo-bar])).`,
query: `json_prolog(Json, json([foo=bar])).`,
wantResult: []testutil.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'])).`,
query: `json_prolog(Json, json([foo=bar,foobar='bar foo'])).`,
wantResult: []testutil.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])).`,
query: `json_prolog(Json, json(['string with space'=bar])).`,
wantResult: []testutil.TermResults{{
"Json": "'{\"string with space\":\"bar\"}'",
}},
wantSuccess: true,
},
{
description: "ensure json follows same order as prolog encoded",
query: `json_prolog(Json, json([b-a,a-b])).`,
query: `json_prolog(Json, json([b=a,a=b])).`,
wantResult: []testutil.TermResults{{
"Json": "'{\"b\":\"a\",\"a\":\"b\"}'",
}},
wantSuccess: true,
},
{
description: "invalid json term compound",
query: `json_prolog(Json, foo([a-b])).`,
query: `json_prolog(Json, foo([a=b])).`,
wantSuccess: false,
wantError: fmt.Errorf("error(type_error(json,foo([-(a,b)])),json_prolog/2)"),
wantError: fmt.Errorf("error(type_error(json,foo([=(a,b)])),json_prolog/2)"),
},
{
description: "convert json term object from prolog with error inside",
Expand All @@ -319,22 +319,28 @@ func TestJsonProlog(t *testing.T) {
},
{
description: "convert json term object from prolog with error inside another object",
query: `json_prolog(Json, ['string with space',json([key-json(error)])]).`,
query: `json_prolog(Json, ['string with space',json([key=json(error)])]).`,
wantSuccess: false,
wantError: fmt.Errorf("error(type_error(list,error),json_prolog/2)"),
},
{
description: "convert json term object which incorrectly defines key/value pair",
query: `json_prolog(Json, json([not_a_pair(key,value)])).`,
query: `json_prolog(Json, json([not_a_key_value(key,value)])).`,
wantSuccess: false,
wantError: fmt.Errorf("error(type_error(pair,not_a_pair(key,value)),json_prolog/2)"),
wantError: fmt.Errorf("error(type_error(key_value,not_a_key_value(key,value)),json_prolog/2)"),
},
{
description: "convert json term object which uses a non atom for key",
query: `json_prolog(Json, json([-(42,value)])).`,
query: `json_prolog(Json, json([=(42,value)])).`,
wantSuccess: false,
wantError: fmt.Errorf("error(type_error(atom,42),json_prolog/2)"),
},
{
description: "convert json term object with arity > 2",
query: `json_prolog(Json, json(a,b,c)).`,
wantSuccess: false,
wantError: fmt.Errorf("error(type_error(json,json(a,b,c)),json_prolog/2)"),
},
// ** Prolog -> JSON **
// Number
{
Expand Down Expand Up @@ -470,7 +476,7 @@ func TestJsonProlog(t *testing.T) {
// ** JSON <-> Prolog **
{
description: "ensure unification doesn't depend on formatting",
query: `json_prolog('{\n\t"foo": "bar"\n}', json( [ foo - bar ] )).`,
query: `json_prolog('{\n\t"foo": "bar"\n}', json( [ foo = bar ] )).`,
wantResult: []testutil.TermResults{{}},
wantSuccess: true,
},
Expand Down Expand Up @@ -542,42 +548,42 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) {
}{
{
json: "'{\"foo\":\"bar\"}'",
term: "json([foo-bar])",
term: "json([foo=bar])",
wantSuccess: true,
},
{
json: "'{\"foo\":\"null\"}'",
term: "json([foo-null])",
term: "json([foo=null])",
wantSuccess: true,
},
{
json: "'{\"foo\":null}'",
term: "json([foo- @(null)])",
term: "json([foo= @(null)])",
wantSuccess: true,
},
{
json: "'{\"employee\":{\"age\":30,\"city\":\"New York\",\"name\":\"John\"}}'",
term: "json([employee-json([age-30.0,city-'New York',name-'John'])])",
term: "json([employee=json([age=30.0,city='New York',name='John'])])",
wantSuccess: true,
},
{
json: "'{\"cosmos\":[\"axone\",{\"name\":\"localnet\"}]}'",
term: "json([cosmos-[axone,json([name-localnet])]])",
term: "json([cosmos=[axone,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.0,2.0,3.0],arrayobject-[json([name-toto]),json([name-tata])],bool- @(true),boolean- @(false),null- @(null)])])",
term: "json([object=json([array=[1.0,2.0,3.0],arrayobject=[json([name=toto]),json([name=tata])],bool= @(true),boolean= @(false),null= @(null)])])",
wantSuccess: true,
},
{
json: "'{\"foo\":\"bar\"}'",
term: "json([a-b])",
term: "json([a=b])",
wantSuccess: false,
},
{
json: `'{"key1":null,"key2":[],"key3":{"nestedKey1":null,"nestedKey2":[],"nestedKey3":["a",null,null]}}'`,
term: `json([key1- @(null),key2-[],key3-json([nestedKey1- @(null),nestedKey2-[],nestedKey3-[a,@(null),@(null)]])])`,
term: `json([key1= @(null),key2=[],key3=json([nestedKey1= @(null),nestedKey2=[],nestedKey3=[a,@(null),@(null)]])])`,
wantSuccess: true,
},
}
Expand Down
31 changes: 28 additions & 3 deletions x/logic/prolog/assert.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,40 @@ func AssertList(term engine.Term, env *engine.Env) (engine.Term, error) {
// AssertPair resolves a term as a pair and returns the pair components.
// If conversion fails, the function returns nil and the error.
func AssertPair(term engine.Term, env *engine.Env) (engine.Term, engine.Term, error) {
return AssertTuple2WithFunctor(term, AtomPair, AtomTypePair, env)
}

// AssertKeyValue resolves a term as a key-value and returns its components, the key as an atom,
// and the value as a term.
// If conversion fails, the function returns nil and the error.
func AssertKeyValue(term engine.Term, env *engine.Env) (engine.Atom, engine.Term, error) {
k, v, err := AssertTuple2WithFunctor(term, AtomKeyValue, AtomTypeKeyValue, env)
if err != nil {
return AtomEmpty, nil, err
}

key, err := AssertAtom(k, env)
if err != nil {
return AtomEmpty, nil, err
}

return key, v, err
}

// AssertTuple2WithFunctor resolves a term as a tuple and returns the tuple components based on the given functor.
// If conversion fails, the function returns nil and an error.
func AssertTuple2WithFunctor(
term engine.Term, functor engine.Atom, functorType engine.Atom, env *engine.Env,
) (engine.Term, engine.Term, error) {
term, err := AssertIsGround(term, env)
if err != nil {
return nil, nil, err
}
if term, ok := term.(engine.Compound); ok && term.Functor() == AtomPair && term.Arity() == 2 {
return term.Arg(0), term.Arg(1), nil
if compound, ok := term.(engine.Compound); ok && compound.Functor() == functor && compound.Arity() == 2 {
return compound.Arg(0), compound.Arg(1), nil
}

return nil, nil, engine.TypeError(AtomTypePair, term, env)
return nil, nil, engine.TypeError(functorType, term, env)
}

// AssertURIComponent resolves a term as a URI component and returns it as an URIComponent.
Expand Down
3 changes: 3 additions & 0 deletions x/logic/prolog/atom.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ var (
// AtomPair are terms with principal functor (-)/2.
// For example, the term -(A, B) denotes the pair of elements A and B.
AtomPair = engine.NewAtom("-")
// AtomKeyValue are terms with principal functor (=)/2.
// For example, the term =(A, B) denotes the mapping of key A with value B.
AtomKeyValue = engine.NewAtom("=")
// AtomPath is the term used to indicate the path component.
AtomPath = engine.NewAtom("path")
// AtomQueryValue is the term used to indicate the query value component.
Expand Down
2 changes: 2 additions & 0 deletions x/logic/prolog/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ var (
AtomTypeOption = engine.NewAtom("option")
// AtomTypePair is the term used to indicate the pair type.
AtomTypePair = engine.NewAtom("pair")
// AtomTypeKeyValue is the term used to indicate the key-value type.
AtomTypeKeyValue = engine.NewAtom("key_value")
// AtomTypeJSON is the term used to indicate the json type.
AtomTypeJSON = engine.NewAtom("json")
// AtomTypeURIComponent is the term used to represent the URI component type.
Expand Down

0 comments on commit 37d4518

Please sign in to comment.