Skip to content

Commit d101d03

Browse files
committed
feat(json): wrap streaming Encoder/Decoder + Number/Delim/Valid + iface aliases
Phase 2 v0.10.4 gap-fill — the biggest genuine gap in the sweep: json.go buffered Marshal/Unmarshal only, but consumers reach for the streaming codec 860+ times (json.NewEncoder 654, json.NewDecoder 207) with no core equivalent at all: - JSONNewEncoder/JSONNewDecoder + JSONEncoder/JSONDecoder type aliases - JSONNumber (13 uses), JSONDelim (5) — UseNumber + token-walk support - JSONValid (2) — well-formedness gate without a throwaway Unmarshal - JSONMarshaler/JSONUnmarshaler interface aliases for custom codecs Additive only. Good/Bad/Ugly per symbol (incl. JSONL multi-value streams, large-int precision via UseNumber), runnable godoc examples, ReportAllocs benchmarks for the streaming hot paths + Valid. Co-Authored-By: Virgil <virgil@lethean.io>
1 parent 9a8d3c0 commit d101d03

4 files changed

Lines changed: 258 additions & 0 deletions

File tree

json.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,82 @@ import "encoding/json"
2727
// }
2828
type RawMessage = json.RawMessage
2929

30+
// JSONNumber is an alias for json.Number — a JSON numeric literal kept
31+
// as its original string so callers choose int vs float decoding (and
32+
// avoid float64 precision loss on large integers). Pairs with the
33+
// UseNumber() option on JSONDecoder.
34+
//
35+
// var n core.JSONNumber = "9007199254740993"
36+
// r := n.Int64() // exact, no float rounding
37+
type JSONNumber = json.Number
38+
39+
// JSONDelim is an alias for json.Delim — one of the four structural
40+
// tokens ( [ ] { } ) returned by JSONDecoder.Token during streaming
41+
// token walks.
42+
//
43+
// tok, _ := dec.Token()
44+
// if d, ok := tok.(core.JSONDelim); ok && d == '[' { /* array start */ }
45+
type JSONDelim = json.Delim
46+
47+
// JSONEncoder is an alias for json.Encoder — the streaming writer that
48+
// emits JSON values to an io.Writer one at a time. Construct via
49+
// JSONNewEncoder.
50+
//
51+
// var enc *core.JSONEncoder = core.JSONNewEncoder(w)
52+
type JSONEncoder = json.Encoder
53+
54+
// JSONDecoder is an alias for json.Decoder — the streaming reader that
55+
// pulls JSON values from an io.Reader. Construct via JSONNewDecoder.
56+
//
57+
// var dec *core.JSONDecoder = core.JSONNewDecoder(r)
58+
type JSONDecoder = json.Decoder
59+
60+
// JSONMarshaler is an alias for json.Marshaler — the interface a type
61+
// implements to control its own JSON encoding.
62+
//
63+
// func (id AgentID) MarshalJSON() ([]byte, error) { ... }
64+
// var _ core.JSONMarshaler = AgentID{}
65+
type JSONMarshaler = json.Marshaler
66+
67+
// JSONUnmarshaler is an alias for json.Unmarshaler — the interface a
68+
// type implements to control its own JSON decoding.
69+
//
70+
// func (id *AgentID) UnmarshalJSON(b []byte) error { ... }
71+
// var _ core.JSONUnmarshaler = (*AgentID)(nil)
72+
type JSONUnmarshaler = json.Unmarshaler
73+
74+
// JSONNewEncoder returns a streaming JSON encoder writing to w. Prefer
75+
// it over JSONMarshal when emitting many values to a stream (HTTP
76+
// response, JSONL file) — it writes incrementally without buffering
77+
// every value into one []byte.
78+
//
79+
// enc := core.JSONNewEncoder(core.Stdout())
80+
// for _, row := range rows { enc.Encode(row) } // one JSON value per line
81+
func JSONNewEncoder(w Writer) *JSONEncoder {
82+
return json.NewEncoder(w)
83+
}
84+
85+
// JSONNewDecoder returns a streaming JSON decoder reading from r. Prefer
86+
// it over JSONUnmarshal when consuming a stream of values or a body of
87+
// unknown length — it decodes one value at a time and can switch to
88+
// JSONNumber mode via dec.UseNumber().
89+
//
90+
// dec := core.JSONNewDecoder(resp.Body)
91+
// var msg Message
92+
// for dec.More() { if err := dec.Decode(&msg); err != nil { break } }
93+
func JSONNewDecoder(r Reader) *JSONDecoder {
94+
return json.NewDecoder(r)
95+
}
96+
97+
// JSONValid reports whether data is a well-formed JSON encoding. Use it
98+
// to gate a payload before storing or forwarding it without paying a
99+
// full Unmarshal into a throwaway target.
100+
//
101+
// if !core.JSONValid(body) { return core.NewError("malformed JSON body") }
102+
func JSONValid(data []byte) bool {
103+
return json.Valid(data)
104+
}
105+
30106
// JSONMarshal serialises a value to JSON bytes.
31107
//
32108
// r := core.JSONMarshal(myStruct)

json_bench_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,29 @@ func BenchmarkJSONUnmarshal_IntoRawMessage(b *B) {
213213
_ = JSONUnmarshal(jsonMedium, &v)
214214
}
215215
}
216+
217+
// --- Streaming Encoder / Decoder / Valid ---
218+
219+
func BenchmarkJSONNewEncoder_Medium(b *B) {
220+
bld := NewBuilder()
221+
b.ReportAllocs()
222+
for i := 0; i < b.N; i++ {
223+
bld.Reset()
224+
_ = JSONNewEncoder(bld).Encode(fixtureMedium)
225+
}
226+
}
227+
228+
func BenchmarkJSONNewDecoder_Medium(b *B) {
229+
b.ReportAllocs()
230+
for i := 0; i < b.N; i++ {
231+
var v benchJSONMedium
232+
_ = JSONNewDecoder(NewReader(jsonMediumStr)).Decode(&v)
233+
}
234+
}
235+
236+
func BenchmarkJSONValid_Medium(b *B) {
237+
b.ReportAllocs()
238+
for i := 0; i < b.N; i++ {
239+
_ = JSONValid(jsonMedium)
240+
}
241+
}

json_example_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,39 @@ func ExampleRawMessage() {
7070
// ping
7171
// {"port":8080}
7272
}
73+
74+
// ExampleJSONNewEncoder streams two values straight to stdout as JSONL
75+
// — one JSON object per line — without buffering the whole batch. Encode
76+
// writes the trailing newline itself.
77+
func ExampleJSONNewEncoder() {
78+
type row struct {
79+
Name string `json:"name"`
80+
}
81+
enc := JSONNewEncoder(Stdout())
82+
enc.Encode(row{Name: "a"})
83+
enc.Encode(row{Name: "b"})
84+
// Output:
85+
// {"name":"a"}
86+
// {"name":"b"}
87+
}
88+
89+
// ExampleJSONNewDecoder pulls a single value from a reader, the
90+
// streaming counterpart to JSONUnmarshal.
91+
func ExampleJSONNewDecoder() {
92+
type cfg struct {
93+
Port int `json:"port"`
94+
}
95+
var c cfg
96+
JSONNewDecoder(NewReader(`{"port":8080}`)).Decode(&c)
97+
Println(c.Port)
98+
// Output: 8080
99+
}
100+
101+
// ExampleJSONValid gates a payload without decoding it.
102+
func ExampleJSONValid() {
103+
Println(JSONValid([]byte(`{"ok":true}`)))
104+
Println(JSONValid([]byte(`{"ok":`)))
105+
// Output:
106+
// true
107+
// false
108+
}

json_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,123 @@ func TestJson_RawMessage_Ugly(t *T) {
164164
AssertTrue(t, r.OK)
165165
AssertEqual(t, `{"a":1}`, string(r.Value.([]byte)))
166166
}
167+
168+
func TestJson_JSONNewEncoder_Good(t *T) {
169+
b := NewBuilder()
170+
err := JSONNewEncoder(b).Encode(testJSON{Name: "stream", Port: 80})
171+
172+
AssertNoError(t, err)
173+
AssertContains(t, b.String(), `"name":"stream"`)
174+
}
175+
176+
func TestJson_JSONNewEncoder_Bad(t *T) {
177+
b := NewBuilder()
178+
err := JSONNewEncoder(b).Encode(make(chan int))
179+
180+
AssertError(t, err)
181+
}
182+
183+
func TestJson_JSONNewEncoder_Ugly(t *T) {
184+
// Encode appends a trailing newline and can be called repeatedly to
185+
// stream multiple values (JSONL).
186+
b := NewBuilder()
187+
enc := JSONNewEncoder(b)
188+
AssertNoError(t, enc.Encode(testJSON{Name: "a", Port: 1}))
189+
AssertNoError(t, enc.Encode(testJSON{Name: "b", Port: 2}))
190+
AssertEqual(t, 2, Count(b.String(), "\n"))
191+
}
192+
193+
func TestJson_JSONNewDecoder_Good(t *T) {
194+
var target testJSON
195+
err := JSONNewDecoder(NewReader(`{"name":"stream","port":80}`)).Decode(&target)
196+
197+
AssertNoError(t, err)
198+
AssertEqual(t, "stream", target.Name)
199+
AssertEqual(t, 80, target.Port)
200+
}
201+
202+
func TestJson_JSONNewDecoder_Bad(t *T) {
203+
var target testJSON
204+
err := JSONNewDecoder(NewReader(`not json`)).Decode(&target)
205+
206+
AssertError(t, err)
207+
}
208+
209+
func TestJson_JSONNewDecoder_Ugly(t *T) {
210+
// More()/Decode loop over a concatenated stream of two objects.
211+
dec := JSONNewDecoder(NewReader(`{"name":"a","port":1}{"name":"b","port":2}`))
212+
var names []string
213+
for dec.More() {
214+
var tj testJSON
215+
AssertNoError(t, dec.Decode(&tj))
216+
names = append(names, tj.Name)
217+
}
218+
AssertEqual(t, []string{"a", "b"}, names)
219+
}
220+
221+
func TestJson_JSONNumber_Good(t *T) {
222+
// UseNumber keeps the literal so a large int survives without float
223+
// rounding.
224+
dec := JSONNewDecoder(NewReader(`{"big":9007199254740993}`))
225+
dec.UseNumber()
226+
var m map[string]JSONNumber
227+
AssertNoError(t, dec.Decode(&m))
228+
v := m["big"].Int64
229+
n, err := v()
230+
AssertNoError(t, err)
231+
AssertEqual(t, int64(9007199254740993), n)
232+
}
233+
234+
func TestJson_JSONNumber_Bad(t *T) {
235+
var n JSONNumber = "not-a-number"
236+
_, err := n.Int64()
237+
AssertError(t, err)
238+
}
239+
240+
func TestJson_JSONNumber_Ugly(t *T) {
241+
var n JSONNumber = "3.14"
242+
AssertEqual(t, "3.14", n.String())
243+
}
244+
245+
func TestJson_JSONDelim_Good(t *T) {
246+
dec := JSONNewDecoder(NewReader(`["x"]`))
247+
tok, err := dec.Token()
248+
AssertNoError(t, err)
249+
d, ok := tok.(JSONDelim)
250+
AssertTrue(t, ok)
251+
AssertEqual(t, "[", d.String())
252+
}
253+
254+
func TestJson_JSONDelim_Bad(t *T) {
255+
// A string token is not a JSONDelim.
256+
dec := JSONNewDecoder(NewReader(`"plain"`))
257+
tok, err := dec.Token()
258+
AssertNoError(t, err)
259+
_, ok := tok.(JSONDelim)
260+
AssertFalse(t, ok)
261+
}
262+
263+
func TestJson_JSONDelim_Ugly(t *T) {
264+
AssertEqual(t, "}", JSONDelim('}').String())
265+
}
266+
267+
func TestJson_JSONValid_Good(t *T) {
268+
AssertTrue(t, JSONValid([]byte(`{"ok":true}`)))
269+
}
270+
271+
func TestJson_JSONValid_Bad(t *T) {
272+
AssertFalse(t, JSONValid([]byte(`{"ok":`)))
273+
}
274+
275+
func TestJson_JSONValid_Ugly(t *T) {
276+
// Empty input is not valid JSON.
277+
AssertFalse(t, JSONValid(nil))
278+
}
279+
280+
func TestJson_JSONUnmarshaler_Good(t *T) {
281+
// The interface alias is satisfied by *testJSON's pointer receiver
282+
// chain via json's default decoding path (structural smoke).
283+
var _ JSONUnmarshaler = (*RawMessage)(nil)
284+
var _ JSONMarshaler = RawMessage(nil)
285+
AssertTrue(t, true)
286+
}

0 commit comments

Comments
 (0)