From 83aa22c11fbdae76121697175da3dfccd5dfe04d Mon Sep 17 00:00:00 2001 From: ccamel Date: Tue, 8 Oct 2024 18:13:53 +0200 Subject: [PATCH] feat(logic): accept JSON as text for json_prolog/2 --- x/logic/predicate/json.go | 155 ++++++++++++++++++++------------------ x/logic/prolog/byte.go | 5 ++ 2 files changed, 86 insertions(+), 74 deletions(-) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 85decf2c..1f94f950 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -2,6 +2,7 @@ package predicate import ( "encoding/json" + "errors" "fmt" "sort" "strconv" @@ -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: // @@ -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) { @@ -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) } @@ -64,14 +61,20 @@ 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 { @@ -79,7 +82,7 @@ func jsonStringToTerms(j engine.Atom, env *engine.Env) (engine.Term, error) { 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) @@ -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: @@ -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) diff --git a/x/logic/prolog/byte.go b/x/logic/prolog/byte.go index 30f5bbb0..499472ea 100644 --- a/x/logic/prolog/byte.go +++ b/x/logic/prolog/byte.go @@ -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)) +}