Documentation
¶
Overview ¶
Package codec is the SDK's JSON layer. Of the SDK's code it is the only package that imports github.com/bytedance/sonic (the benchmark comparator internal/testsupport/naive imports it too, and the tests of the root package and of internal/testsupport may import its root package), and it holds one of the module's two non-test uses of unsafe, the zero-copy bytes-to-string bridge NoCopyString; the root package's typed store is the other.
It builds only where sonic's JIT path does, Go 1.17 to 1.27 on amd64 and arm64; everywhere else the package is unsupported.go alone and fails to compile on purpose (docs/support.md). The seam tests in seam_test.go confine unsafe to those places, and sonic too save for its root package in the tests of the root package and of internal/testsupport, keep every JSON library out of the non-test code of the SDK's other packages, whose tests may use encoding/json to check what a caller's encoder sees, and check the two build constraints file by file.
The package holds the request body's scratch pool (Body), the request encoder that writes a state or a body member into it (EncodeState, EncodeValue, AppendRawState, AppendRawValue), the response decoder (DecodeSystemOne, DecodeModels: one ast.Preorder traversal that validates every token, a trailing-data check, and a lazy second pass for structured legends), the lenient reader of unsuccessful responses' bodies (ReadErrorBody), the per-string check the decoder runs (ValidString) and the encoder's Pretouch hook. Decoded values are the std-only types of internal/wire, which the root package wraps.
Index ¶
- Constants
- Variables
- func AppendRawState(buf *[]byte, raw []byte) error
- func AppendRawValue(buf *[]byte, raw []byte) error
- func DecodeModels(body []byte, dst *wire.ModelList) error
- func EncodeState(buf *[]byte, state any) error
- func EncodeValue(buf *[]byte, v any) error
- func NoCopyString(b []byte) string
- func Pretouch(t reflect.Type) error
- func ValidString(s string) bool
- type Body
- type BodyReader
- type DecodeError
- type EncodeError
- type ErrorBody
- type FieldPath
- type Skipped
- type SkippedAnswer
Constants ¶
const DecoderCeiling = 4 << 20
DecoderCeiling is the most scratch, in bytes as scratchBytes bounds it, that a decoder keeps when it goes back to the pool. A decode that grew the scratch past it leaves the scratch to the garbage collector, and the decoder goes back without it, as a request body's scratch past ScratchCeiling does. Without the ceiling, one body of tiny answers inside the 16 MiB response cap can leave about 233 MiB in the pool, kept for as long as later decodes reuse that decoder. It is 4 MiB: 10^4 answer entries, the size of the largest collection a decode budget pins (structured-legend-flood-10k's legend), take 2 880 000 bytes, and the next power of two is 4 MiB; the largest scratch a pinned fixture leaves, that flood's, is 2 172 168 bytes by scratchBytes, so every pinned decode keeps its warm decoder (docs/perf/frozen-budgets.md).
const MaxSkipped = 8
MaxSkipped is the number of skipped answers a decode reports by name: the SDK logs one WARN line for each, and one line counting the rest.
const ( // ScratchCeiling is the largest capacity a scratch buffer may have and // still go back to the pool when its body is released. A body that grew // past it is left to the garbage collector, so the pool never retains // more than this per buffer. ScratchCeiling = 8 << 20 )
Variables ¶
var ( // ErrStateShape reports a state that is not a JSON string, array or // object. The API takes text, a JSON object or an array as the state; // null, booleans and numbers are refused before any request is sent. ErrStateShape = errors.New("not a string, an array or an object") // ErrPlainBytes reports a state or a body member value that is a plain // []byte: sonic would send it as a base64 string, which is rarely what // the caller meant (text, or JSON that is already encoded). ErrPlainBytes = errors.New("a plain []byte is ambiguous") // ErrRawValue reports raw JSON that does not start a JSON value. ErrRawValue = errors.New("not a JSON value") )
var ( // ErrBodyClosed is returned by a Read on a [BodyReader] that has been // closed. ErrBodyClosed = errors.New("read on closed request body") // ErrBodyReleased is returned by [Body.Open] once every reference to the // body has been dropped, and by Open through a [Body] whose scratch // already serves a later call. ErrBodyReleased = errors.New("request body already released") )
Functions ¶
func AppendRawState ¶
AppendRawState appends raw, an encoded state, to *buf as it is, after an O(1) shape check: its first byte other than JSON whitespace must be '{', '[' or '"' (ErrStateShape otherwise, including for raw that is empty or only whitespace). raw is not otherwise checked: that it is one valid JSON value is the caller's contract.
func AppendRawValue ¶
AppendRawValue appends raw, one encoded JSON value, to *buf as it is, after an O(1) check that its first byte other than JSON whitespace can start a JSON value (ErrRawValue otherwise, including for raw that is empty or only whitespace). raw is not otherwise checked.
func DecodeModels ¶
DecodeModels decodes the body of a successful list-models response into *dst, which it overwrites, with the checks of DecodeSystemOne. The result never aliases body: the cards' strings are copies sharing one allocation. It fails with a *DecodeError at "models", at "models[i]" or at "models[i].<member>", the first card first and its members in schema order.
func EncodeState ¶
EncodeState appends the JSON encoding of a request state to *buf with sonic's encoder.EncodeInto and checks the result: it must be a JSON string, array or object (ErrStateShape otherwise: nil, booleans, numbers, and nil maps, slices and pointers, which encode as null), and valid UTF-8 (wire.ErrInvalidUTF8 otherwise: sonic copies the bytes of a Go string as they are, and a body with an invalid byte is not JSON text). A plain []byte, or a byte slice of a named type that sonic would also send as base64 (plainBytes), fails with ErrPlainBytes; a []byte nested inside the state is sent as a base64 string, as encoding/json does. A value sonic cannot encode fails with an *EncodeError.
Floats keep sonic's spelling, which is encoding/json's except that a negative zero loses its sign on arm64: 3.0 is written 3, -0.0 as 0 on arm64 and as -0 on amd64, and 1e16 <= |x| < 1e21 and 1e-6 <= |x| < 1e-5 in fixed digits. Raw JSON appended with AppendRawState, or a number carried as a string, keeps an exact spelling. A map's members are written in Go's iteration order, which changes from one encode to the next: a struct or raw JSON gives stable bytes.
The output of a json.Marshaler in the state, such as a nested json.RawMessage, is checked only by sonic, whose check does not refuse every invalid output; that it is valid JSON is the caller's contract, as for raw JSON appended with AppendRawState. The state is not scanned again as a whole.
On failure *buf keeps its length from before the call. The checks cost one pass over the encoded bytes and allocate nothing.
func EncodeValue ¶
EncodeValue appends the JSON encoding of v, any JSON value, to *buf with sonic's encoder.EncodeInto, for a member of the request body other than the state. Unlike EncodeState it takes null, booleans and numbers; the output must still be valid UTF-8. A plain []byte, or a byte slice of a named type (plainBytes), fails with ErrPlainBytes, as for the state, and a []byte nested inside v is sent as a base64 string. A value sonic cannot encode fails with an *EncodeError. On failure *buf keeps its length from before the call.
Floats and map members follow EncodeState's rules. Raw JSON appended with AppendRawValue, or a number carried as a string, keeps an exact spelling.
func NoCopyString ¶
NoCopyString returns b viewed as a string, without copying it. It is one of the module's two non-test uses of unsafe, with the typed decode's field store in the root package: the decoder hands a response body to sonic's parser as a string, and sonic returns keys and strings without escape sequences as substrings of it.
The string aliases b. The aliasing contract is that b must not be modified for as long as the string, or any substring of it, is reachable: a write to b would change a string Go treats as immutable. A response body satisfies this, since the SDK never writes to it after the read; a pooled buffer that is reused does not, so a string that must outlive its buffer is copied (the decoder's interning either returns the request's own string or copies).
func Pretouch ¶
Pretouch compiles sonic's encoder for values of type t ahead of the first request, so that the one-time JIT compilation is not paid inside a call (the root package's WithPretouch option calls it).
func ValidString ¶
ValidString reports whether s is valid UTF-8 and free of the raw control characters U+0000 to U+001F, which JSON forbids inside a string and the Python SDK's parser rejects. sonic accepts both (invalid UTF-8 and raw control characters pass every sonic path), so the response decoder runs this check on the strings it is handed. It makes one pass over s and allocates nothing.
s must be the raw text between a JSON string's quotes, before escape sequences are decoded. sonic's ast.Visitor delivers exactly that for a string without escape sequences; for a string with one it delivers the decoded value, in which a legal escape such as \n or \u0000 has become a control character that this check would reject. The caller keeps the two cases apart.
Types ¶
type Body ¶
type Body struct {
// contains filtered or unexported fields
}
Body is a request body encoded into a pooled scratch buffer. Once built it is immutable and shared by every reader the transport opens on it: the request's Body and each GetBody result of a replay or a retry.
A body is reference-counted. NewBody returns it holding one reference, the SDK call's own, which Body.Release drops when the call returns; each Body.Open adds one for the returned reader, which the reader's Close drops. The buffer goes back to the pool only when the count reaches zero, so a retry never re-sends bytes that another call has overwritten. A reader the transport never closes (the HTTP/2 replay path may drop one) keeps the count above zero: its buffer is then garbage-collected with it and never reused.
A Body value is a handle: the pooled scratch and the generation NewBody gave it. Once the count has reached zero the scratch may serve a later call under a new generation, and every handle of the earlier one is stale: Open through it fails with ErrBodyReleased and Release does nothing, so a handle kept past its call can neither read nor free the next call's body. The generation has 32 bits: a handle would have to outlive 2^32 reuses of its scratch to match again. The zero Body is not usable.
func NewBody ¶
func NewBody() Body
NewBody takes a body with an empty scratch buffer from the pool. It holds the caller's reference, which the caller drops with Body.Release.
func (Body) Buffer ¶
Buffer returns the scratch buffer for appending the encoded body, as sonic's encoder.EncodeInto takes it. It may be used only while the body is being built, before the first Body.Open.
func (Body) Bytes ¶
Bytes returns the encoded body. The slice is shared: it must not be modified, and it is valid only while the caller holds a reference.
func (Body) GetBody ¶
func (b Body) GetBody() (io.ReadCloser, error)
GetBody is Body.Open in the form of http.Request.GetBody: it returns a new reader over the body, or a nil reader and ErrBodyReleased. A request that sends the body sets GetBody to this method, so that a replay or a retry reads the same bytes.
func (Body) Open ¶
func (b Body) Open() (*BodyReader, error)
Open returns a new reader over the body, holding one more reference until its Close. It fails with ErrBodyReleased once the count has reached zero or the handle is stale. Open is safe for concurrent use with every other method except Body.Buffer.
type BodyReader ¶
type BodyReader struct {
// contains filtered or unexported fields
}
BodyReader reads one copy of a Body. It implements io.ReadCloser for http.Request.Body and GetBody.
Read and Close are safe for concurrent use: the HTTP transports may close a request body from another goroutine while a write loop is still reading it. Close waits for an in-flight Read to return, so the buffer is never recycled under a reader.
func (*BodyReader) Close ¶
func (r *BodyReader) Close() error
Close drops the reader's reference to the body after any in-flight Read has returned. It is idempotent: a compare-and-swap on the closed flag lets only the first call drop the reference. It always returns nil.
func (*BodyReader) Read ¶
func (r *BodyReader) Read(p []byte) (int, error)
Read reads the next bytes of the body. It returns ErrBodyClosed after Close, and io.EOF at the end of the body.
type DecodeError ¶
type DecodeError struct {
// Path is where the body failed.
Path FieldPath
// Err is the reason.
Err error
}
DecodeError reports a response body the decoder refused: Path is where, Err is why. Err is one of the decoder's reasons or, for a body that is not JSON, sonic's error; Error never prints the body or Path's server-chosen names.
func (*DecodeError) Error ¶
func (e *DecodeError) Error() string
Error returns a sentence naming the reason: "invalid JSON: …" for a failure of the JSON layer, "invalid response data: …" for a member. sonic's traversal reports a syntax error by a fixed description without a position; its lazy pass reports one with a position and a quotation of the body, of which only the position is kept.
type EncodeError ¶
type EncodeError struct {
Err error
}
EncodeError reports a value that sonic cannot encode: NaN or an infinity, a type JSON has no form for (a channel, a function, a complex number), a value nested too deep (a cycle), or a json.Marshaler whose output is not valid JSON. Err is sonic's error.
func (*EncodeError) Error ¶
func (e *EncodeError) Error() string
Error returns sonic's message, except for a json.Marshaler whose output is not valid JSON, such as a nested RawJSON or JSON Content: sonic's message quotes that output, which is the caller's data, so only the position sonic reports is kept; for a truncated output it can lie past the end, and it can differ from one build to another (-race). Unwrap still gives sonic's error.
type ErrorBody ¶
type ErrorBody struct {
// Message is the server's message, found as the Python SDK finds it
// (errors.py extract_message): the body when it is a JSON string or not
// JSON at all, else "error" when it is a string, "error.message",
// "message", "detail" when it is a string, "detail.message", or the
// "msg" of each "detail" entry prefixed with its "loc" path and joined
// with "; ". When none of those holds a non-empty string, Message is the
// body itself: its JSON compacted (whitespace between tokens dropped,
// nothing else re-encoded), or a JSON string's value, which may be
// empty. A body that is not JSON is its text with each ill-formed UTF-8
// sequence replaced by U+FFFD. Message is neither escaped nor cut.
Message string
// NoBody reports a body that is empty or the JSON null, which the Python
// SDK reports as "status code (no body)".
NoBody bool
// ErrorType is the server's machine-readable name for the failure, from
// "detail.error_type" when it is a string, or empty.
ErrorType string
}
ErrorBody is what the lenient error-body reader found in the body of an unsuccessful response.
func ReadErrorBody ¶
ReadErrorBody reads the body of an unsuccessful response as the Python SDK does (errors.py extract_message over json.py deserialize). The body is JSON only if it is exactly one valid JSON value, with valid UTF-8 and no raw control character in any string, as the Python SDK's parser requires; anything else is text. A duplicate member takes its last value. It never fails and never aliases body.
type FieldPath ¶
type FieldPath struct {
// Top is the top-level member: "model", "usage", "answers" or "models".
// It is empty for a failure of the JSON layer, whose path is the root.
Top string
// Index is the position of a model card in "models", when HasIndex is
// set.
Index int
// HasIndex reports whether the path goes through a model card.
HasIndex bool
// Name is the answer's name, when HasName is set. A name may be empty.
Name string
// HasName reports whether the path goes through an answer.
HasName bool
// Member is the member inside the answer, the usage or the model card,
// or empty.
Member string
// Key is the probability or legend key, when HasKey is set.
Key string
// HasKey reports whether the path ends at a probability or legend key.
HasKey bool
}
FieldPath locates a failure in a response body the way the Python SDK's field_path names it: dotted member names, a bracketed index for a model card, and the root for a failure of the JSON layer. Name and Key are text the server chose; whoever prints them escapes and cuts them.
type Skipped ¶
type Skipped struct {
// Count is the number of skipped answers.
Count int
// contains filtered or unexported fields
}
Skipped lists the answers a decode dropped because their type is one this version does not model, in the order their names first appear, as the Python SDK logs them: every such answer of the last "answers" member before the first answer that is not an object or has no string type, even when the decode then fails. The names and types alias the body.
func DecodeSystemOne ¶
func DecodeSystemOne(body []byte, q *wire.Prepared, model string, dst *wire.SystemOneResult) (Skipped, error)
DecodeSystemOne decodes the body of a successful System One response into *dst, which it overwrites. q is the question set the request asked, and model the model it named; either may be nil or empty.
The body is traversed once by sonic's ast.Preorder, which validates every token, including those of members the SDK does not read; every string and key must be valid UTF-8 without a raw control character; only JSON whitespace may follow the top-level object. Unknown members are ignored. A repeated member takes its last value at every level, and a repeated answer name keeps the position of its first appearance (a Python dict). An answer whose type this version does not model is dropped and reported in the returned Skipped. When an answer's legend holds a JSON object or array, one lazy second pass reads its exact bytes.
The result never aliases body: every string of the answers, and the model, is the request's own when it equals one (a question name, an option label, a level's text or compact JSON, the model), and a copy otherwise, all copies of one decode sharing one allocation.
It fails with a *DecodeError naming the first failure the Python SDK would report: a body that is not one JSON object at the root; then the first answer that is not an object or has no string type; then model, usage and answers in that order; then, in the order the answers' names first appear, the first member of the answer in schema order that is missing, of the wrong kind, or holds a bad key or value.
func DecodeSystemOneInto ¶
func DecodeSystemOneInto(body []byte, q *wire.Prepared, model string, dst *wire.SystemOneResult, spare []wire.AnswerEntry) (Skipped, error)
DecodeSystemOneInto is DecodeSystemOne with room for the answers: when spare's capacity covers the answers the decode keeps, dst's answers are stored in spare's array (wire.Answers.GrowInto) and cost no allocation; a smaller spare, or nil, is not used. The caller gives up spare's array to dst.
func (*Skipped) Named ¶
func (s *Skipped) Named() []SkippedAnswer
Named returns the skipped answers reported by name.
type SkippedAnswer ¶
type SkippedAnswer struct {
// Name is the answer's name.
Name string
// Type is its type.
Type string
}
SkippedAnswer is an answer of a type this version of the SDK does not model, which the decoder dropped.