diff --git a/x/logic/predicate/address.go b/x/logic/predicate/address.go index f3fc017b..7bc9f6fc 100644 --- a/x/logic/predicate/address.go +++ b/x/logic/predicate/address.go @@ -5,9 +5,10 @@ import ( "fmt" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/prolog" bech322 "github.com/cosmos/cosmos-sdk/types/bech32" + + "github.com/okp4/okp4d/x/logic/prolog" ) // Bech32Address is a predicate that convert a [bech32] encoded string into [base64] bytes and give the address prefix, @@ -44,7 +45,7 @@ func Bech32Address(vm *engine.VM, address, bech32 engine.Term, cont engine.Cont, if err != nil { return engine.Error(fmt.Errorf("bech32_address/2: failed to decode Bech32: %w", err)) } - pair := prolog.AtomPair.Apply(prolog.StringToTerm(h), prolog.BytesToCodepointListTermWithDefault(a)) + pair := prolog.AtomPair.Apply(prolog.StringToTerm(h), prolog.BytesToCodepointListTermWithDefault(a, env)) return engine.Unify(vm, address, pair, cont, env) default: return engine.Error(fmt.Errorf("bech32_address/2: invalid Bech32 type: %T, should be Atom or Variable", b)) @@ -70,7 +71,7 @@ func addressPairToBech32(addressPair engine.Compound, env *engine.Env) (string, switch a := env.Resolve(addressPair.Arg(1)).(type) { case engine.Compound: - data, err := prolog.StringTermToBytes(a, "", env) + data, err := prolog.StringTermToBytes(a, prolog.AtomEmpty, env) if err != nil { return "", fmt.Errorf("failed to convert term to bytes list: %w", err) } diff --git a/x/logic/predicate/address_test.go b/x/logic/predicate/address_test.go index 581d75ca..2a59f576 100644 --- a/x/logic/predicate/address_test.go +++ b/x/logic/predicate/address_test.go @@ -89,7 +89,7 @@ func TestBech32(t *testing.T) { }, { query: `bech32_address(-('okp4', ['8956',167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`, - wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: invalid character_code '8956' value in list at position 1: should be a single character"), + wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: error(domain_error(valid_character_code(8956),[8956,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]),bech32_address/2)"), wantSuccess: false, }, { @@ -99,7 +99,7 @@ func TestBech32(t *testing.T) { }, { query: `bech32_address(-('okp4', hey(2)), Bech32).`, - wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: invalid compound term: expected a list of character_code or integer"), + wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: error(type_error(character_code,hey(2)),bech32_address/2)"), wantSuccess: false, }, { diff --git a/x/logic/predicate/bank.go b/x/logic/predicate/bank.go index e1077c03..06ebf6f9 100644 --- a/x/logic/predicate/bank.go +++ b/x/logic/predicate/bank.go @@ -5,10 +5,10 @@ import ( "fmt" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/prolog" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/okp4/okp4d/x/logic/prolog" "github.com/okp4/okp4d/x/logic/types" "github.com/okp4/okp4d/x/logic/util" ) diff --git a/x/logic/predicate/crypto.go b/x/logic/predicate/crypto.go index c05e6af3..846000f2 100644 --- a/x/logic/predicate/crypto.go +++ b/x/logic/predicate/crypto.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/prolog" + "github.com/okp4/okp4d/x/logic/prolog" "github.com/okp4/okp4d/x/logic/util" ) @@ -79,7 +79,7 @@ func CryptoDataHash( return engine.Error(fmt.Errorf("%s: failed to hash data: %w", functor, err)) } - return engine.Unify(vm, hash, prolog.BytesToCodepointListTermWithDefault(result), cont, env) + return engine.Unify(vm, hash, prolog.BytesToCodepointListTermWithDefault(result, env), cont, env) }) } @@ -226,7 +226,7 @@ func termToBytes(term, options, defaultEncoding engine.Term, env *engine.Env) ([ case prolog.AtomHex: return prolog.TermHexToBytes(term, env) case prolog.AtomOctet, prolog.AtomUtf8: - return prolog.StringTermToBytes(term, "", env) + return prolog.StringTermToBytes(term, prolog.AtomEmpty, env) default: return nil, fmt.Errorf("invalid encoding: %s. Possible values: hex, octet", encodingAtom.String()) } diff --git a/x/logic/predicate/did.go b/x/logic/predicate/did.go index 88ce7b6e..a6268270 100644 --- a/x/logic/predicate/did.go +++ b/x/logic/predicate/did.go @@ -7,6 +7,7 @@ import ( "github.com/ichiban/prolog/engine" godid "github.com/nuts-foundation/go-did/did" + "github.com/okp4/okp4d/x/logic/prolog" ) diff --git a/x/logic/predicate/did_test.go b/x/logic/predicate/did_test.go index 3c46aade..b1a35e67 100644 --- a/x/logic/predicate/did_test.go +++ b/x/logic/predicate/did_test.go @@ -94,7 +94,7 @@ func TestDID(t *testing.T) { query: `did_components(X,did(example,'123456','path with/space',5,test)).`, wantResult: []types.TermResults{}, wantError: fmt.Errorf( - "did_components/2: failed to resolve atom at segment 3: invalid term '%%!s(engine.Integer=5)' - expected engine.Atom but got engine.Integer"), //nolint:lll + "did_components/2: failed to resolve atom at segment 3: error(type_error(atom,5),did_components/2)"), }, { query: `did_components('did:example:123456',foo(X)).`, diff --git a/x/logic/predicate/encoding.go b/x/logic/predicate/encoding.go index 6b04720a..e15f747a 100644 --- a/x/logic/predicate/encoding.go +++ b/x/logic/predicate/encoding.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/ichiban/prolog/engine" + "github.com/okp4/okp4d/x/logic/prolog" ) @@ -45,9 +46,9 @@ func HexBytes(vm *engine.VM, hexa, bts engine.Term, cont engine.Cont, env *engin if result == nil { return engine.Error(fmt.Errorf("hex_bytes/2: nil hexadecimal conversion in input")) } - return engine.Unify(vm, bts, prolog.BytesToCodepointListTermWithDefault(result), cont, env) + return engine.Unify(vm, bts, prolog.BytesToCodepointListTermWithDefault(result, env), cont, env) case engine.Compound: - src, err := prolog.StringTermToBytes(b, "", env) + src, err := prolog.StringTermToBytes(b, prolog.AtomEmpty, env) if err != nil { return engine.Error(fmt.Errorf("hex_bytes/2: %w", err)) } diff --git a/x/logic/predicate/encoding_test.go b/x/logic/predicate/encoding_test.go index af90fd83..e00ecc34 100644 --- a/x/logic/predicate/encoding_test.go +++ b/x/logic/predicate/encoding_test.go @@ -68,7 +68,7 @@ func TestHexBytesPredicate(t *testing.T) { query: `hex_bytes('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae', [345,38,'hey',107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`, wantSuccess: false, - wantError: fmt.Errorf("hex_bytes/2: invalid integer value '345' in list at position 1: out of byte range (0-255)"), + wantError: fmt.Errorf("hex_bytes/2: error(domain_error(valid_byte(345),[345,38,hey,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]),hex_bytes/2)"), //nolint:lll }, } for nc, tc := range cases { diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 72a1eabc..520a03cc 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -8,12 +8,13 @@ import ( "strings" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/prolog" "github.com/samber/lo" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/okp4/okp4d/x/logic/prolog" ) // JSONProlog is a predicate that will unify a JSON string into prolog terms and vice versa. @@ -123,13 +124,13 @@ func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) { } switch { - case prolog.JsonBool(true).Compare(t, env) == 0: + case prolog.JSONBool(true).Compare(t, env) == 0: return json.Marshal(true) - case prolog.JsonBool(false).Compare(t, env) == 0: + case prolog.JSONBool(false).Compare(t, env) == 0: return json.Marshal(false) - case prolog.JsonEmptyArray().Compare(t, env) == 0: + case prolog.JSONEmptyArray().Compare(t, env) == 0: return json.Marshal([]json.RawMessage{}) - case prolog.JsonNull().Compare(t, env) == 0: + case prolog.JSONNull().Compare(t, env) == 0: return json.Marshal(nil) } @@ -153,9 +154,9 @@ func jsonToTerms(value any) (engine.Term, error) { } return engine.Integer(r.Int64()), nil case bool: - return prolog.JsonBool(v), nil + return prolog.JSONBool(v), nil case nil: - return prolog.JsonNull(), nil + return prolog.JSONNull(), nil case map[string]any: keys := lo.Keys(v) sort.Strings(keys) @@ -172,7 +173,7 @@ func jsonToTerms(value any) (engine.Term, error) { case []any: elements := make([]engine.Term, 0, len(v)) if len(v) == 0 { - return prolog.JsonEmptyArray(), nil + return prolog.JSONEmptyArray(), nil } for _, element := range v { diff --git a/x/logic/predicate/string.go b/x/logic/predicate/string.go index 9969adac..d1cbd741 100644 --- a/x/logic/predicate/string.go +++ b/x/logic/predicate/string.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/ichiban/prolog/engine" + "github.com/okp4/okp4d/x/logic/prolog" ) @@ -114,11 +115,11 @@ func StringBytes( return engine.Error(err) } forwardConverter := func(value []engine.Term, options engine.Term, env *engine.Env) ([]engine.Term, error) { - bs, err := prolog.StringTermToBytes(value[0], encodingAtom.String(), env) + bs, err := prolog.StringTermToBytes(value[0], encodingAtom, env) if err != nil { return nil, err } - result, err := prolog.BytesToCodepointListTerm(bs, "text") + result, err := prolog.BytesToCodepointListTerm(bs, prolog.AtomText, env) if err != nil { return nil, err } @@ -128,11 +129,11 @@ func StringBytes( if _, err := prolog.AssertList(env, value[0]); err != nil { return nil, err } - bs, err := prolog.StringTermToBytes(value[0], "text", env) + bs, err := prolog.StringTermToBytes(value[0], prolog.AtomText, env) if err != nil { return nil, err } - result, err := prolog.BytesToAtomListTerm(bs, encodingAtom.String()) + result, err := prolog.BytesToAtomListTerm(bs, encodingAtom, env) if err != nil { return nil, err } diff --git a/x/logic/predicate/uri.go b/x/logic/predicate/uri.go index 8eb23a1c..eced3453 100644 --- a/x/logic/predicate/uri.go +++ b/x/logic/predicate/uri.go @@ -6,6 +6,7 @@ import ( "net/url" "github.com/ichiban/prolog/engine" + "github.com/okp4/okp4d/x/logic/prolog" ) diff --git a/x/logic/predicate/util.go b/x/logic/predicate/util.go index 389ec34e..c0074e30 100644 --- a/x/logic/predicate/util.go +++ b/x/logic/predicate/util.go @@ -3,10 +3,11 @@ package predicate import ( "sort" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/prolog" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/okp4/okp4d/x/logic/prolog" "github.com/okp4/okp4d/x/logic/types" ) diff --git a/x/logic/prolog/assert.go b/x/logic/prolog/assert.go index edf9bf2d..2dcf2486 100644 --- a/x/logic/prolog/assert.go +++ b/x/logic/prolog/assert.go @@ -98,6 +98,24 @@ func AssertAtom(env *engine.Env, t engine.Term) (engine.Atom, error) { return AtomEmpty, engine.TypeError(AtomAtom, t, env) } +// AssertCharacterCode resolves a term and attempts to convert it into an engine.Integer if possible. +// If conversion fails, the function returns the zero value and the error. +func AssertCharacterCode(env *engine.Env, t engine.Term) (engine.Integer, error) { + if t, ok := env.Resolve(t).(engine.Integer); ok { + return t, nil + } + return 0, engine.TypeError(AtomCharacterCode, t, env) +} + +// AssertCharacter resolves a term and attempts to convert it into an engine.Atom if possible. +// If conversion fails, the function returns the empty atom and the error. +func AssertCharacter(env *engine.Env, t engine.Term) (engine.Atom, error) { + if t, ok := env.Resolve(t).(engine.Atom); ok { + return t, nil + } + return AtomEmpty, engine.TypeError(AtomCharacter, t, env) +} + // AssertList resolves a term as a list and returns it as a engine.Compound. // If conversion fails, the function returns nil and the error. func AssertList(env *engine.Env, t engine.Term) (engine.Compound, error) { diff --git a/x/logic/prolog/assert_test.go b/x/logic/prolog/assert_test.go index 38e7b104..0ce42442 100644 --- a/x/logic/prolog/assert_test.go +++ b/x/logic/prolog/assert_test.go @@ -4,10 +4,11 @@ import ( "fmt" "testing" - "github.com/okp4/okp4d/x/logic/util" "github.com/samber/lo" . "github.com/smartystreets/goconvey/convey" + + "github.com/okp4/okp4d/x/logic/util" ) var ( diff --git a/x/logic/prolog/atom.go b/x/logic/prolog/atom.go index 2aca2356..24740523 100644 --- a/x/logic/prolog/atom.go +++ b/x/logic/prolog/atom.go @@ -2,27 +2,54 @@ package prolog import "github.com/ichiban/prolog/engine" -// Common atoms. var ( - AtomAs = engine.NewAtom("as") // AtomAs is the term used to indicate the as encoding type option. - AtomAt = engine.NewAtom("@") // AtomAt are terms with principal functor (@)/1. It is used to represent special values in json objects. - AtomAtom = engine.NewAtom("atom") // AtomAtom is the term used to indicate the atom atom, - AtomCharset = engine.NewAtom("charset") // AtomCharset is the term used to indicate the charset encoding type option. - AtomCompound = engine.NewAtom("compound") // AtomCompound is the term used to indicate the atom compound, - AtomDot = engine.NewAtom(".") // AtomDot is the term used to represent the dot in a list. - AtomEmpty = engine.NewAtom("") // AtomEmpty is the term used to represent empty. - AtomEmptyArray = engine.NewAtom("[]") // AtomEmptyArray is the term []. - AtomEmptyList = engine.NewAtom("[]") // AtomEmptyList is the term used to represent an empty list. - AtomEncoding = engine.NewAtom("encoding") // AtomEncoding is the term used to indicate the encoding type option. - AtomError = engine.NewAtom("error") // AtomError is the term used to indicate the error. - AtomFalse = engine.NewAtom("false") // AtomFalse is the term false. - AtomHex = engine.NewAtom("hex") // AtomHex is the term used to indicate the hexadecimal encoding type option. - AtomJSON = engine.NewAtom("json") // AtomJSON are terms with principal functor json/1. // It is used to represent json objects. - AtomList = engine.NewAtom("list") // AtomList is the term used to indicate the atom list, - AtomNull = engine.NewAtom("null") // AtomNull is the term null. - AtomOctet = engine.NewAtom("octet") // AtomOctet is the term used to indicate the byte encoding type option. - AtomPadding = engine.NewAtom("padding") // AtomPadding is the term used to indicate the padding encoding type option. - AtomPair = engine.NewAtom("-") // AtomPair are terms with principal functor (-)/2. For example, the term -(A, B) denotes the pair of elements A and B. - AtomTrue = engine.NewAtom("true") // AtomTrue is the term true. - AtomUtf8 = engine.NewAtom("utf8") // AtomUtf8 is the term used to indicate the UTF-8 encoding type option. + // AtomAs is the term used to indicate the as encoding type option. + AtomAs = engine.NewAtom("as") + // AtomAt are terms with principal functor (@)/1 used to represent special values in json objects. + AtomAt = engine.NewAtom("@") + // AtomAtom is the term used to indicate the atom atom. + AtomAtom = engine.NewAtom("atom") + // AtomCharacter is the term used to indicate the character type. + AtomCharacter = engine.NewAtom("character") + // AtomCharacterCode is the term used to indicate the character code type. + AtomCharacterCode = engine.NewAtom("character_code") + // AtomCharset is the term used to indicate the charset encoding type option. + AtomCharset = engine.NewAtom("charset") + // AtomCompound is the term used to indicate the atom compound. + AtomCompound = engine.NewAtom("compound") + // AtomDot is the term used to represent the dot in a list. + AtomDot = engine.NewAtom(".") + // AtomEmpty is the term used to represent empty. + AtomEmpty = engine.NewAtom("") + // AtomEmptyArray is the term []. + AtomEmptyArray = engine.NewAtom("[]") + // AtomEmptyList is the term used to represent an empty list. + AtomEmptyList = engine.NewAtom("[]") + // AtomEncoding is the term used to indicate the encoding type option. + AtomEncoding = engine.NewAtom("encoding") + // AtomError is the term used to indicate the error. + AtomError = engine.NewAtom("error") + // AtomFalse is the term false. + AtomFalse = engine.NewAtom("false") + // AtomHex is the term used to indicate the hexadecimal encoding type option. + AtomHex = engine.NewAtom("hex") + // AtomJSON are terms with principal functor json/1 used to represent json objects. + AtomJSON = engine.NewAtom("json") + // AtomList is the term used to indicate the atom list. + AtomList = engine.NewAtom("list") + // AtomNull is the term null. + AtomNull = engine.NewAtom("null") + // AtomOctet is the term used to indicate the byte encoding type option. + AtomOctet = engine.NewAtom("octet") + // AtomPadding is the term used to indicate the padding encoding type option. + AtomPadding = engine.NewAtom("padding") + // AtomPair are terms with principal functor (-)/2. + // For example, the term -(A, B) denotes the pair of elements A and B. + AtomPair = engine.NewAtom("-") + // AtomText is the term used to indicate the atom text. + AtomText = engine.NewAtom("text") + // AtomTrue is the term true. + AtomTrue = engine.NewAtom("true") + // AtomUtf8 is the term used to indicate the UTF-8 encoding type option. + AtomUtf8 = engine.NewAtom("utf8") ) diff --git a/x/logic/prolog/encoding.go b/x/logic/prolog/encoding.go new file mode 100644 index 00000000..5a2fb500 --- /dev/null +++ b/x/logic/prolog/encoding.go @@ -0,0 +1,73 @@ +package prolog + +import ( + "bytes" + "unicode/utf8" + + "github.com/ichiban/prolog/engine" + "golang.org/x/net/html/charset" +) + +// Decode converts a byte slice from a specified encoding. +// Decode function is the reverse of encode function. +func Decode(bs []byte, label engine.Atom, env *engine.Env) ([]byte, error) { + switch label { + case AtomEmpty, AtomText: + return bs, nil + case AtomOctet: + var buffer bytes.Buffer + for _, b := range bs { + buffer.WriteRune(rune(b)) + } + return buffer.Bytes(), nil + default: + encoding, _ := charset.Lookup(label.String()) + if encoding == nil { + return nil, engine.DomainError(ValidCharset(), label, env) + } + result, err := encoding.NewDecoder().Bytes(bs) + if err != nil { + culprit := BytesToCodepointListTermWithDefault(bs, env) + return nil, engine.DomainError(ValidEncoding(label.String(), err), culprit, env) + } + return result, nil + } +} + +// Encode converts a byte slice to a specified encoding. +// +// In case of: +// - empty encoding label or 'text', return the original bytes without modification. +// - 'octet', decode the bytes as unicode code points and return the resulting bytes. If a code point is greater than +// 0xff, an error is returned. +// - any other encoding label, convert the bytes to the specified encoding. +func Encode(bs []byte, label engine.Atom, env *engine.Env) ([]byte, error) { + switch label { + case AtomEmpty, AtomText: + return bs, nil + case AtomOctet: + result := make([]byte, 0, len(bs)) + for i := 0; i < len(bs); { + runeValue, width := utf8.DecodeRune(bs[i:]) + + if runeValue > 0xff { + culprit := BytesToCodepointListTermWithDefault(bs, env) + return nil, engine.DomainError(ValidByte(int64(runeValue)), culprit, env) + } + result = append(result, byte(runeValue)) + i += width + } + return result, nil + default: + encoding, _ := charset.Lookup(label.String()) + if encoding == nil { + return nil, engine.DomainError(ValidCharset(), label, env) + } + result, err := encoding.NewEncoder().Bytes(bs) + if err != nil { + culprit := BytesToCodepointListTermWithDefault(bs, env) + return nil, engine.DomainError(ValidEncoding(label.String(), err), culprit, env) + } + return result, nil + } +} diff --git a/x/logic/prolog/error.go b/x/logic/prolog/error.go index f4484e0b..ea9c30d4 100644 --- a/x/logic/prolog/error.go +++ b/x/logic/prolog/error.go @@ -2,12 +2,31 @@ package prolog import "github.com/ichiban/prolog/engine" -// Error atoms var ( - AtomEncodingError = engine.NewAtom("encoding_error") // AtomEncodingError is the term used to indicate the encoding error. + AtomDomainError = engine.NewAtom("domain_error") // AtomDomainError is the atom domain_error. + AtomValidByte = engine.NewAtom("valid_byte") // AtomValidByte is the atom valid_byte. + AtomValidCharset = engine.NewAtom("valid_charset") // AtomValidCharset is the atom valid_charset. + AtomValidCharacterCode = engine.NewAtom("valid_character_code") // AtomValidCharacterCode is the atom valid_character_code. + AtomValidEncoding = engine.NewAtom("valid_encoding") // AtomValidEncoding is the atom valid_encoding. + AtomValidHexDigit = engine.NewAtom("valid_hex_digit") // AtomValidHexDigit is the atom valid_hex_digit. ) -// EncodingError returns the compound term error(encoding_error(Encoding, cause)). -func EncodingError(encoding string, cause error, env *engine.Env) engine.Exception { - return engine.NewException(AtomError.Apply(AtomEncodingError.Apply(engine.NewAtom(encoding)), StringToTerm(cause.Error())), env) +func ValidCharset() engine.Term { + return AtomValidCharset +} + +func ValidEncoding(encoding string, cause error) engine.Term { + return AtomValidEncoding.Apply(engine.NewAtom(encoding), StringToStringTerm(cause.Error())) +} + +func ValidByte(v int64) engine.Term { + return AtomValidByte.Apply(engine.Integer(v)) +} + +func ValidCharacterCode(c string) engine.Term { + return AtomValidCharacterCode.Apply(engine.NewAtom(c)) +} + +func ValidHexDigit(d string) engine.Term { + return AtomValidHexDigit.Apply(engine.NewAtom(d)) } diff --git a/x/logic/prolog/json.go b/x/logic/prolog/json.go index a8eb267a..75a15789 100644 --- a/x/logic/prolog/json.go +++ b/x/logic/prolog/json.go @@ -6,14 +6,14 @@ import ( "github.com/ichiban/prolog/engine" ) -// JsonNull returns the compound term @(null). +// JSONNull returns the compound term @(null). // It is used to represent the null value in json objects. -func JsonNull() engine.Term { +func JSONNull() engine.Term { return AtomAt.Apply(AtomNull) } -// JsonBool returns the compound term @(true) if b is true, otherwise @(false). -func JsonBool(b bool) engine.Term { +// JSONBool returns the compound term @(true) if b is true, otherwise @(false). +func JSONBool(b bool) engine.Term { if b { return AtomAt.Apply(AtomTrue) } @@ -21,9 +21,9 @@ func JsonBool(b bool) engine.Term { return AtomAt.Apply(AtomFalse) } -// JsonEmptyArray returns is the compound term @([]). +// JSONEmptyArray returns is the compound term @([]). // It is used to represent the empty array in json objects. -func JsonEmptyArray() engine.Term { +func JSONEmptyArray() engine.Term { return AtomAt.Apply(AtomEmptyArray) } diff --git a/x/logic/prolog/json_test.go b/x/logic/prolog/json_test.go index 7ae9b6ce..d6bb750f 100644 --- a/x/logic/prolog/json_test.go +++ b/x/logic/prolog/json_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ichiban/prolog/engine" + . "github.com/smartystreets/goconvey/convey" ) diff --git a/x/logic/prolog/list.go b/x/logic/prolog/list.go new file mode 100644 index 00000000..afb2a237 --- /dev/null +++ b/x/logic/prolog/list.go @@ -0,0 +1,12 @@ +package prolog + +import "github.com/ichiban/prolog/engine" + +// ListHead returns the first element of the given list. +func ListHead(list engine.Term, env *engine.Env) engine.Term { + iter := engine.ListIterator{List: list, Env: env} + if !iter.Next() { + return nil + } + return iter.Current() +} diff --git a/x/logic/prolog/term.go b/x/logic/prolog/term.go index a77f9905..2b0d59ff 100644 --- a/x/logic/prolog/term.go +++ b/x/logic/prolog/term.go @@ -2,10 +2,9 @@ package prolog import ( "encoding/hex" - "fmt" + "unicode/utf8" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/util" ) // Tuple is a predicate which unifies the given term with a tuple of the given arity. @@ -23,13 +22,24 @@ func ListOfIntegers(args ...int) engine.Term { } // StringToTerm converts a string to a term. +// TODO: this function should be removed. func StringToTerm(s string) engine.Term { return engine.NewAtom(s) } +// StringToStringTerm converts a string to a term representing a list of characters. +func StringToStringTerm(s string) engine.Term { + terms := make([]engine.Term, 0, utf8.RuneCountInString(s)) + for _, c := range s { + terms = append(terms, engine.NewAtom(string(c))) + } + + return engine.List(terms...) +} + // BytesToCodepointListTerm try to convert a given golang []byte into a list of codepoints. -func BytesToCodepointListTerm(in []byte, encoding string) (engine.Term, error) { - out, err := util.Decode(in, encoding) +func BytesToCodepointListTerm(in []byte, encoding engine.Atom, env *engine.Env) (engine.Term, error) { + out, err := Decode(in, encoding, env) if err != nil { return nil, err } @@ -43,8 +53,8 @@ func BytesToCodepointListTerm(in []byte, encoding string) (engine.Term, error) { // BytesToCodepointListTermWithDefault is like the BytesToCodepointListTerm function but with a default encoding. // This function panics if the conversion fails, which can't happen with the default encoding. -func BytesToCodepointListTermWithDefault(in []byte) engine.Term { - term, err := BytesToCodepointListTerm(in, "") +func BytesToCodepointListTermWithDefault(in []byte, env *engine.Env) engine.Term { + term, err := BytesToCodepointListTerm(in, AtomEmpty, env) if err != nil { panic(err) } @@ -52,8 +62,8 @@ func BytesToCodepointListTermWithDefault(in []byte) engine.Term { } // BytesToAtomListTerm try to convert a given golang []byte into a list of atoms, one for each character. -func BytesToAtomListTerm(in []byte, encoding string) (engine.Term, error) { - out, err := util.Decode(in, encoding) +func BytesToAtomListTerm(in []byte, encoding engine.Atom, env *engine.Env) (engine.Term, error) { + out, err := Decode(in, encoding, env) if err != nil { return nil, err } @@ -74,61 +84,90 @@ func BytesToAtomListTerm(in []byte, encoding string) (engine.Term, error) { // - any other encoding label, convert the bytes to the specified encoding. // // The mapping from encoding labels to encodings is defined at https://encoding.spec.whatwg.org/. -func StringTermToBytes(str engine.Term, encoding string, env *engine.Env) (bs []byte, err error) { +func StringTermToBytes(str engine.Term, encoding engine.Atom, env *engine.Env) (bs []byte, err error) { v := env.Resolve(str) switch v := v.(type) { case engine.Atom: - if bs, err = util.Encode([]byte(v.String()), encoding); err != nil { - return nil, EncodingError(encoding, err, env) + if bs, err = Encode([]byte(v.String()), encoding, env); err != nil { + return nil, err } - return + return bs, nil case engine.Compound: if IsList(v) { - iter := engine.ListIterator{List: v, Env: env} - bs := make([]byte, 0) - index := 0 - - for iter.Next() { - term := env.Resolve(iter.Current()) - index++ - - switch t := term.(type) { - case engine.Integer: - if t >= 0 && t <= 255 { - bs = append(bs, byte(t)) - } else { - return nil, fmt.Errorf("invalid integer value '%d' in list at position %d: out of byte range (0-255)", t, index) - } - case engine.Atom: - rs := []rune(t.String()) - if len(rs) != 1 { - return nil, fmt.Errorf("invalid character_code '%s' value in list at position %d: should be a single character", - t.String(), index) - } - - bs = append(bs, []byte(t.String())...) - default: - return nil, fmt.Errorf("invalid term type in list at position %d: %T, only character_code or integer allowed", index, term) + head := ListHead(v, env) + if head == nil { + return make([]byte, 0), nil + } + + switch head.(type) { + case engine.Atom: + if bs, err = characterListToBytes(v, env); err != nil { + return bs, err } + case engine.Integer: + if bs, err = characterCodeListToBytes(v, env); err != nil { + return bs, err + } + default: + return nil, engine.TypeError(AtomCharacterCode, v, env) } - return util.Encode(bs, encoding) + return Encode(bs, encoding, env) } - return nil, fmt.Errorf("invalid compound term: expected a list of character_code or integer") + return nil, engine.TypeError(AtomCharacterCode, str, env) default: - return nil, fmt.Errorf("term should be a List, given %T", str) + return nil, engine.TypeError(AtomText, str, env) + } +} + +func characterListToBytes(str engine.Compound, env *engine.Env) ([]byte, error) { + iter := engine.ListIterator{List: str, Env: env} + bs := make([]byte, 0) + + for iter.Next() { + e, err := AssertCharacter(env, iter.Current()) + if err != nil { + return bs, err + } + rs := []rune(e.String()) + if len(rs) != 1 { + return bs, engine.DomainError(ValidCharacterCode(e.String()), str, env) + } + + bs = append(bs, []byte(e.String())...) } + return bs, nil +} + +func characterCodeListToBytes(str engine.Compound, env *engine.Env) ([]byte, error) { + iter := engine.ListIterator{List: str, Env: env} + bs := make([]byte, 0) + + for iter.Next() { + e, err := AssertCharacterCode(env, iter.Current()) + if err != nil { + return nil, err + } + if e < 0 || e > 255 { + return nil, engine.DomainError(ValidByte(int64(e)), str, env) + } + + bs = append(bs, byte(e)) + } + return bs, nil } // TermHexToBytes try to convert an hexadecimal encoded atom to native golang []byte. func TermHexToBytes(term engine.Term, env *engine.Env) ([]byte, error) { - v := env.Resolve(term) - switch v := v.(type) { - case engine.Atom: - src := []byte(v.String()) - result := make([]byte, hex.DecodedLen(len(src))) - _, err := hex.Decode(result, src) - return result, err - default: - return nil, fmt.Errorf("invalid term: expected a hexadecimal encoded atom, given %T", term) + v, err := AssertAtom(env, term) + if err != nil { + return nil, err + } + + src := []byte(v.String()) + result := make([]byte, hex.DecodedLen(len(src))) + _, err = hex.Decode(result, src) + if err != nil { + err = engine.DomainError(ValidEncoding("hex", err), term, env) } + return result, err } diff --git a/x/logic/prolog/term_test.go b/x/logic/prolog/term_test.go index f5cfc68e..9f64246c 100644 --- a/x/logic/prolog/term_test.go +++ b/x/logic/prolog/term_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - . "github.com/smartystreets/goconvey/convey" - "github.com/ichiban/prolog/engine" + + . "github.com/smartystreets/goconvey/convey" ) func TestTermHexToBytes(t *testing.T) { @@ -27,7 +27,7 @@ func TestTermHexToBytes(t *testing.T) { term: engine.NewAtom("foo").Apply(engine.NewAtom("bar")), result: nil, wantSuccess: false, - wantError: fmt.Errorf("invalid term: expected a hexadecimal encoded atom, given *engine.compound"), + wantError: fmt.Errorf("error(type_error(atom,foo(bar)),_2)"), }, } for nc, tc := range cases { @@ -46,10 +46,10 @@ func TestTermHexToBytes(t *testing.T) { }) } else { Convey("then error should occurs", func() { - So(err, ShouldNotBeNil) + So(err, ShouldNotEqual, nil) Convey("and should be as expected", func() { - So(err, ShouldResemble, tc.wantError) + So(err.Error(), ShouldEqual, tc.wantError.Error()) }) }) } @@ -167,7 +167,7 @@ func TestTermToBytes(t *testing.T) { Convey(fmt.Sprintf("Given the term #%d: %s", nc, tc.term), func() { Convey("when converting string term to bytes", func() { env := engine.Env{} - result, err := StringTermToBytes(tc.term, tc.encoding, &env) + result, err := StringTermToBytes(tc.term, engine.NewAtom(tc.encoding), &env) if tc.wantSuccess { Convey("then no error should be thrown", func() { diff --git a/x/logic/util/encoding.go b/x/logic/util/encoding.go deleted file mode 100644 index f1a1f5ec..00000000 --- a/x/logic/util/encoding.go +++ /dev/null @@ -1,62 +0,0 @@ -package util - -import ( - "bytes" - "fmt" - "unicode/utf8" - - "golang.org/x/net/html/charset" -) - -// Decode converts a byte slice from a specified encoding. -// Decode function is the reverse of encode function. -func Decode(bs []byte, label string) ([]byte, error) { - switch label { - case "", "text": - return bs, nil - case "octet": - var buffer bytes.Buffer - for _, b := range bs { - buffer.WriteRune(rune(b)) - } - return buffer.Bytes(), nil - default: - encoding, _ := charset.Lookup(label) - if encoding == nil { - return nil, fmt.Errorf("invalid encoding: %s", label) - } - return encoding.NewDecoder().Bytes(bs) - } -} - -// Encode converts a byte slice to a specified encoding. -// -// In case of: -// - empty encoding label or 'text', return the original bytes without modification. -// - 'octet', decode the bytes as unicode code points and return the resulting bytes. If a code point is greater than -// 0xff, an error is returned. -// - any other encoding label, convert the bytes to the specified encoding. -func Encode(bs []byte, label string) ([]byte, error) { - switch label { - case "", "text": - return bs, nil - case "octet": - result := make([]byte, 0, len(bs)) - for i := 0; i < len(bs); { - runeValue, width := utf8.DecodeRune(bs[i:]) - - if runeValue > 0xff { - return nil, fmt.Errorf("cannot convert character '%c' to %s", runeValue, label) - } - result = append(result, byte(runeValue)) - i += width - } - return result, nil - default: - encoding, _ := charset.Lookup(label) - if encoding == nil { - return nil, fmt.Errorf("invalid encoding: %s", label) - } - return encoding.NewEncoder().Bytes(bs) - } -}