Skip to content

Commit

Permalink
feat(logic): accept JSON as text for json_prolog/2
Browse files Browse the repository at this point in the history
  • Loading branch information
ccamel committed Oct 8, 2024
1 parent fa1e24a commit 83aa22c
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 74 deletions.
155 changes: 81 additions & 74 deletions x/logic/predicate/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package predicate

import (
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
Expand All @@ -15,17 +16,15 @@ import (
"github.com/axone-protocol/axoned/v10/x/logic/prolog"
)

// JSONProlog is a predicate that will unify a JSON string into prolog terms and vice versa.
// JSONProlog is a predicate that unifies a JSON into a prolog term and vice versa.
//
// The signature is as follows:
//
// json_prolog(?Json, ?Term) is det
//
// 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.
// - Json is the textual representation of the json, as an atom, a list of character codes, or list of characters.
// - Term is a term that represents the JSON in the prolog world.
//
// # Examples:
//
Expand All @@ -36,14 +35,12 @@ func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin

switch t1 := env.Resolve(j).(type) {
case engine.Variable:
case engine.Atom:
terms, err := jsonStringToTerms(t1, env)
default:
terms, err := decodeJSONToTerm(t1, env)
if err != nil {
return engine.Error(err)
}
result = terms
default:
return engine.Error(engine.TypeError(prolog.AtomTypeAtom, j, env))
}

switch t2 := env.Resolve(term).(type) {
Expand All @@ -53,7 +50,7 @@ func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin
}
return engine.Unify(vm, term, result, cont, env)
default:
b, err := termsToJSON(t2, env)
b, err := encodeTermToJSON(t2, env)
if err != nil {
return engine.Error(err)
}
Expand All @@ -64,22 +61,28 @@ func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin
prolog.WithError(
engine.DomainError(prolog.ValidEncoding("json"), term, env), err, env))
}
var r engine.Term = engine.NewAtom(string(b))
var r engine.Term = prolog.BytesToAtom(b)
return engine.Unify(vm, j, r, cont, env)
}
}

func jsonStringToTerms(j engine.Atom, env *engine.Env) (engine.Term, error) {
// decodeJSONToTerm decode a JSON, given as a prolog text, into a prolog term.
func decodeJSONToTerm(j engine.Term, env *engine.Env) (engine.Term, error) {
payload, err := prolog.TextTermToString(j, env)
if err != nil {
return nil, err
}

var values any
decoder := json.NewDecoder(strings.NewReader(j.String()))
decoder := json.NewDecoder(strings.NewReader(payload))
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, prolog.WithError(
engine.DomainError(prolog.ValidEncoding("json"), j, env), err, env)
}

term, err := jsonToTerms(values)
term, err := jsonToTerm(values)
if err != nil {
return nil, prolog.WithError(
engine.DomainError(prolog.ValidEncoding("json"), j, env), err, env)
Expand All @@ -88,83 +91,87 @@ func jsonStringToTerms(j engine.Atom, env *engine.Env) (engine.Term, error) {
return term, nil
}

func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) {
asDomainError := func(bs []byte, err error) ([]byte, error) {
if err != nil {
return bs, prolog.WithError(
engine.DomainError(prolog.ValidEncoding("json"), term, env), err, env)
}
return bs, err
// encodeTermToJSON converts a Prolog term to a JSON byte array.
func encodeTermToJSON(term engine.Term, env *engine.Env) ([]byte, error) {
bs, err := termToJSON(term, env)

var exception engine.Exception
if err != nil && !errors.As(err, &exception) {
return nil, prolog.WithError(engine.DomainError(prolog.ValidEncoding("json"), term, env), err, env)
}

return bs, err
}

func termToJSON(term engine.Term, env *engine.Env) ([]byte, error) {
switch t := term.(type) {
case engine.Atom:
return asDomainError(json.Marshal(t.String()))
return json.Marshal(t.String())
case engine.Integer:
return asDomainError(json.Marshal(t))
return json.Marshal(t)
case engine.Float:
return asDomainError(func() ([]byte, error) {
str := t.String()
float, err := strconv.ParseFloat(str, 64)
if err != nil {
return nil, err
}
float, err := strconv.ParseFloat(t.String(), 64)
if err != nil {
return nil, err
}

return json.Marshal(float)
}())
return json.Marshal(float)
case engine.Compound:
switch {
case t.Functor() == prolog.AtomDot:
iter, err := prolog.ListIterator(t, env)
return compoundToJSON(t, env)
}

return nil, engine.TypeError(prolog.AtomTypeJSON, term, env)
}

func compoundToJSON(term engine.Compound, env *engine.Env) ([]byte, error) {
switch {
case term.Functor() == prolog.AtomDot:
iter, err := prolog.ListIterator(term, env)
if err != nil {
return nil, err
}

elements := make([]json.RawMessage, 0)
for iter.Next() {
element, err := termToJSON(iter.Current(), env)
if err != nil {
return nil, err
}
elements = append(elements, element)
}
return json.Marshal(elements)
case term.Functor() == prolog.AtomJSON:
terms, err := prolog.ExtractJSONTerm(term, env)
if err != nil {
return nil, err
}

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 asDomainError(json.Marshal(elements))
case t.Functor() == prolog.AtomJSON:
terms, err := prolog.ExtractJSONTerm(t, env)
attributes := make(map[string]json.RawMessage, len(terms))
for key, term := range terms {
raw, err := termToJSON(term, 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 asDomainError(json.Marshal(attributes))
case prolog.JSONBool(true).Compare(t, env) == 0:
return asDomainError(json.Marshal(true))
case prolog.JSONBool(false).Compare(t, env) == 0:
return asDomainError(json.Marshal(false))
case prolog.JSONEmptyArray().Compare(t, env) == 0:
return asDomainError(json.Marshal([]json.RawMessage{}))
case prolog.JSONNull().Compare(t, env) == 0:
return asDomainError(json.Marshal(nil))
default:
// no-op
attributes[key] = raw
}
default:
// no-op
return json.Marshal(attributes)
case prolog.JSONBool(true).Compare(term, env) == 0:
return json.Marshal(true)
case prolog.JSONBool(false).Compare(term, env) == 0:
return json.Marshal(false)
case prolog.JSONEmptyArray().Compare(term, env) == 0:
return json.Marshal([]json.RawMessage{})
case prolog.JSONNull().Compare(term, env) == 0:
return json.Marshal(nil)
}

return nil, engine.TypeError(prolog.AtomTypeJSON, term, env)
}

func jsonToTerms(value any) (engine.Term, error) {
func jsonToTerm(value any) (engine.Term, error) {
switch v := value.(type) {
case string:
return engine.NewAtom(v), nil
return prolog.StringToAtom(v), nil
case json.Number:
return engine.NewFloatFromString(v.String())
case bool:
Expand All @@ -177,26 +184,26 @@ func jsonToTerms(value any) (engine.Term, error) {

attributes := make([]engine.Term, 0, len(v))
for _, key := range keys {
attributeValue, err := jsonToTerms(v[key])
attributeValue, err := jsonToTerm(v[key])
if err != nil {
return nil, err
}
attributes = append(attributes, prolog.AtomPair.Apply(engine.NewAtom(key), attributeValue))
attributes = append(attributes, prolog.AtomPair.Apply(prolog.StringToAtom(key), attributeValue))
}
return prolog.AtomJSON.Apply(engine.List(attributes...)), nil
case []any:
elements := make([]engine.Term, 0, len(v))
if len(v) == 0 {
return prolog.JSONEmptyArray(), nil
}

elements := make([]engine.Term, 0, len(v))
for _, element := range v {
term, err := jsonToTerms(element)
term, err := jsonToTerm(element)
if err != nil {
return nil, err
}
elements = append(elements, term)
}

return engine.List(elements...), nil
default:
return nil, fmt.Errorf("unsupported type: %T", v)
Expand Down
5 changes: 5 additions & 0 deletions x/logic/prolog/byte.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ func BytesToByteListTerm(in []byte) engine.Term {
}
return engine.List(terms...)
}

// BytesToAtom converts a given golang []byte into an Atom.
func BytesToAtom(in []byte) engine.Atom {
return engine.NewAtom(string(in))
}

0 comments on commit 83aa22c

Please sign in to comment.