module json2 import time import strings import sync.stdatomic // EncoderOptions provides a list of options for encoding @[params] pub struct EncoderOptions { pub: // json5 permits JSON5 output, including Infinity and NaN. json5 bool // These formatting options apply only when json5 is true. single_quotes bool unquoted_keys bool trailing_commas bool prettify bool indent_string string = ' ' newline_string string = '\n' enum_as_int bool escape_unicode bool // time_as_unix encodes a `time.Time` as its Unix timestamp in seconds, like the // removed `json` module did, instead of an RFC 3339 string. time_as_unix bool // legacy_layout, with `prettify: true`, lays the output out like `encode_pretty` // of the removed `json` module: members indented with tabs, a tab after each // key, and array elements on one line, separated by `, `. legacy_layout bool } struct Encoder { EncoderOptions mut: level int prefix string output strings.Builder = strings.new_builder(128) } // encode encodes a value as JSON, or JSON5 with `json5: true`. pub fn encode[T](val T, config EncoderOptions) string { mut encoder := Encoder{ EncoderOptions: config } defer { unsafe { encoder.output.free() } } encoder.encode_value[T](val) return encoder.output.str() } // encode_append appends the JSON representation of val to destination, preserving // its existing bytes and reusing its capacity. Clear destination before calling // to replace its contents. The caller owns the buffer and must not mutate it from // another thread during encoding or pass a value that aliases its storage. @[manualfree] pub fn encode_append[T](val T, mut destination []u8, config EncoderOptions) { mut encoder := Encoder{ EncoderOptions: config // Only this encoder accesses the caller's buffer until it is returned below. output: unsafe { destination } } encoder.encode_value[T](val) destination = unsafe { encoder.output } } fn (mut encoder Encoder) encode_value[T](val T) { $if T is $interface { encoder.encode_null() } $else $if T is $option { // Options outside of struct fields (sum type variants, array elements, map // values, the top-level value) must still produce a JSON value. if val == none { encoder.encode_null() } else { encoder.encode_value(get_value_from_optional(val)) } } $else $if T.unaliased_typ is voidptr { encoder.encode_null() } $else $if T.unaliased_typ is string { encoder.encode_string(string(val)) } $else $if T.unaliased_typ is bool { encoder.encode_boolean(bool(val)) } $else $if T.unaliased_typ is rune { // Like the removed `json` module, a rune is a JSON string of its character. encoder.encode_string(rune(val).str()) } $else $if T.unaliased_typ is u8 { encoder.encode_number(u8(val)) } $else $if T.unaliased_typ is u16 { encoder.encode_number(u16(val)) } $else $if T.unaliased_typ is u32 { encoder.encode_number(u32(val)) } $else $if T.unaliased_typ is u64 { encoder.encode_number(u64(val)) } $else $if T.unaliased_typ is i8 { encoder.encode_number(i8(val)) } $else $if T.unaliased_typ is i16 { encoder.encode_number(i16(val)) } $else $if T.unaliased_typ is int { encoder.encode_number(int(val)) } $else $if T.unaliased_typ is i32 { encoder.encode_number(i32(val)) } $else $if T.unaliased_typ is i64 { encoder.encode_number(i64(val)) } $else $if T.unaliased_typ is isize { encoder.encode_number(i64(val)) } $else $if T.unaliased_typ is uint { encoder.encode_number(uint(val)) } $else $if T.unaliased_typ is f32 { encoder.encode_number(f32(val)) } $else $if T.unaliased_typ is f64 { encoder.encode_number(f64(val)) } $else $if T.unaliased_typ is voidptr { encoder.encode_number(0) } $else $if T is $pointer { if voidptr(val) == unsafe { nil } { encoder.encode_null() } else { encoder.encode_value(*val) } } $else $if T.unaliased_typ is $array_fixed { encoder.output << `[` // JSON5 and the legacy layout spread fixed size arrays like dynamic arrays. spread := encoder.prettify && (encoder.legacy_layout || encoder.json5) if spread { encoder.open_items(val.len, true) } for i in 0 .. val.len { if i > 0 { if spread { encoder.separate_items(true) } else { encoder.output << `,` } } encoder.encode_value(val[i]) } if spread || encoder.json5 { encoder.close_items(val.len, true) } encoder.output << `]` } $else $if T.unaliased_typ is $array { encoder.encode_array(val) } $else $if T.unaliased_typ is $map { encoder.encode_map(val) } $else $if T.unaliased_typ is $enum { if encoder.enum_as_int || enum_uses_json_as_number[T]() { encoder.encode_enum_number(val) } else { mut enum_val := 'unknown enum value' $for member in T.values { if member.value == val { enum_val = member.name for attr in member.attrs { if json_attr := json_attr_value(attr) { enum_val = json_attr } } } } // A `@[json: '...']` name is arbitrary text; escape it like any string. encoder.encode_string(enum_val) } } $else $if T.unaliased_typ is $sumtype { encoder.encode_sumtype[T](val) } $else $if T.unaliased_typ is time.Time { // `time_as_unix` covers aliases of `time.Time` too, like the removed module; // otherwise the value's own (or inherited) `to_json` applies. if encoder.time_as_unix { encoder.encode_number(time.Time(val).unix()) } else { time_val := val.to_json() encoder.output.write_string(time_val) } } $else $if T is JsonEncoder { // uses T, because alias could be implementing JsonEncoder, while the base type does not integer_val := val.to_json() encoder.output.write_string(integer_val) } $else $if T is Encodable { // uses T, because alias could be implementing JsonEncoder, while the base type does not integer_val := val.json_str() encoder.output.write_string(integer_val) } $else $if T.unaliased_typ is $struct { unsafe { $for field in T.fields { $if field.is_embed { encoder.encode_struct_with_embeds(val) return } } encoder.output << `{` is_first := encoder.encode_plain_struct_fields[T](val, true) encoder.close_object(!is_first) } } } // next_string_escape returns the next byte needing JSON escaping, or val.len. @[direct_array_access; inline] fn next_string_escape(val string, start int, escape_unicode bool) int { mut i := start for i <= val.len - 8 { mut word := u64(0) // The length check bounds every load; memcpy also permits unaligned strings. unsafe { vmemcpy(&word, val.str + i, 8) } // A byte below 0x20 leaves a high bit after subtraction and masking. control := (word - u64(0x2020202020202020)) & ~word & u64(0x8080808080808080) if control != 0 || word_has_byte(word, `"`) || word_has_byte(word, `\\`) || (escape_unicode && word & u64(0x8080808080808080) != 0) { break } i += 8 } // Locate an escape within a flagged word, or handle the final short tail. for i < val.len { b := val[i] if b < 0x20 || b == `"` || b == `\\` || (escape_unicode && b >= 0x80) { break } i++ } return i } @[manualfree] fn (mut encoder Encoder) encode_string(val string) { if encoder.json5 { encoder.encode_json5_string(val) return } encoder.output << `"` mut start := 0 mut end := 0 for end < val.len { character := val[end] escape := character < 0x20 || character in [`"`, `\\`] unicode := encoder.escape_unicode && character >= 0x80 if !escape && !unicode { end = next_string_escape(val, end + 1, encoder.escape_unicode) continue } unsafe { encoder.output.push_many(val.str + start, end - start) } end += encoder.encode_escape(val, end) start = end } unsafe { encoder.output.push_many(val.str + start, end - start) } encoder.output << `"` } fn (mut encoder Encoder) encode_escape(val string, index int) int { character := val[index] if encoder.escape_unicode && character >= 0x80 { code_point, width := utf8_rune_at(val, index) encoder.encode_unicode(code_point) return width } escaped := match character { `"`, `\\` { character } `\b` { u8(`b`) } `\n` { u8(`n`) } `\f` { u8(`f`) } `\t` { u8(`t`) } `\r` { u8(`r`) } else { u8(0) } } if escaped != 0 { encoder.output << `\\` encoder.output << escaped return 1 } encoder.encode_unicode(rune(character)) return 1 } fn (mut encoder Encoder) encode_unicode(code_point rune) { if code_point <= 0xffff { encoder.encode_unicode_escape(u32(code_point), false) return } value := u32(code_point) - 0x10000 encoder.encode_unicode_escape(0xD800 + ((value >> 10) & 0x3FF), true) encoder.encode_unicode_escape(0xDC00 + (value & 0x3FF), false) } // encode_unicode_escape writes a UTF-16 code unit without allocating formatted text. fn (mut encoder Encoder) encode_unicode_escape(code u32, uppercase bool) { hex := if uppercase { '0123456789ABCDEF' } else { '0123456789abcdef' } encoder.output << `\\` encoder.output << `u` for shift := 12; shift >= 0; shift -= 4 { encoder.output << hex[int((code >> shift) & 0xf)] } } // utf8_rune_at decodes the UTF-8 sequence at `val[i]` like `string.runes()` does: an // invalid or truncated sequence (arbitrary bytes such as `0xff`) is U+FFFD with a // width of 1, like in the removed `json` module, so every byte is consumed once. @[direct_array_access] fn utf8_rune_at(val string, i int) (rune, int) { b0 := val[i] if b0 < 0x80 { return rune(b0), 1 } if b0 < 0xc2 || b0 >= 0xf5 { return 0xfffd, 1 } width := if b0 < 0xe0 { 2 } else if b0 < 0xf0 { 3 } else { 4 } if i + width > val.len { return 0xfffd, 1 } for j in 1 .. width { if val[i + j] & 0xc0 != 0x80 { return 0xfffd, 1 } } b1 := val[i + 1] if (b0 == 0xe0 && b1 < 0xa0) || (b0 == 0xed && b1 >= 0xa0) || (b0 == 0xf0 && b1 < 0x90) || (b0 == 0xf4 && b1 > 0x8f) { return 0xfffd, 1 } code_point := match width { 2 { ((rune(b0) & 0x1f) << 6) | (rune(b1) & 0x3f) } 3 { ((rune(b0) & 0x0f) << 12) | ((rune(b1) & 0x3f) << 6) | (rune(val[i + 2]) & 0x3f) } else { ((rune(b0) & 0x07) << 18) | ((rune(b1) & 0x3f) << 12) | ((rune(val[i + 2]) & 0x3f) << 6) | (rune(val[i + 3]) & 0x3f) } } return code_point, width } // encode_enum_number writes the backing value of an enum, like the removed `json` // module: `int(val)` would truncate a 64 bit backing value, and misread a large // unsigned one as negative. fn (mut encoder Encoder) encode_enum_number[T](val T) { // The comparison happens in the enum's C type, which is unsigned for an unsigned // backing type. if unsafe { T(-1) } < unsafe { T(0) } { encoder.encode_number(i64(val)) } else { mut bits := u64(val) if sizeof(T) < 8 { // A narrow enum is passed as an `int`, which sign-extends large values. bits &= (u64(1) << (sizeof(T) * 8)) - 1 } encoder.encode_number(bits) } } fn (mut encoder Encoder) encode_boolean(val bool) { if val { encoder.output.write_string(true_string) } else { encoder.output.write_string(false_string) } } @[manualfree] fn (mut encoder Encoder) encode_number[T](val T) { $if T is $int { $if T is u8 || T is u16 || T is u32 || T is u64 || T is uint { encoder.output.write_u_decimal(u64(val)) } $else { encoder.output.write_decimal(i64(val)) } return } mut integer_val := '' $if T is f32 { $if js { integer_val = f32(val).str() } $else { magnitude := f32_abs(val) integer_val = if magnitude == 0 || (magnitude >= f32(1e-6) && magnitude < f32(1e21)) { f32(val).strlong() } else { f32(val).str() } } } $else $if T is f64 { $if js { integer_val = f64(val).str() } $else { magnitude := f64_abs(val) integer_val = if magnitude == 0 || (magnitude >= 1e-6 && magnitude < 1e21) { f64(val).strlong() } else { f64(val).str() } } } defer { unsafe { integer_val.free() } } $if T is $float { // JSON has no NaN or infinity, which V formats as `nan`, `+inf` and `-inf`. if integer_val == 'nan' || integer_val.ends_with('inf') { if encoder.json5 { special := if integer_val == 'nan' { 'NaN' } else if integer_val.starts_with('-') { '-Infinity' } else { 'Infinity' } unsafe { encoder.output.push_many(special.str, special.len) } } else { encoder.encode_null() } return } // JSON uses compact exponents, so `1e-07` becomes `1e-7`. if integer_val.len >= 4 && integer_val[integer_val.len - 4] == `e` && integer_val[integer_val.len - 2] == `0` { integer_val = integer_val[..integer_val.len - 2] + integer_val[integer_val.len - 1..] } if integer_val.len > 2 && integer_val[integer_val.len - 2] == `.` && integer_val[integer_val.len - 1] == `0` { // ends in .0 // `2.0` = > `2` // but skip float in scientific notation, `1e+10` unsafe { integer_val.len -= 2 } } } encoder.output.write_string(integer_val) } @[markused] fn (mut encoder Encoder) encode_null() { encoder.output.write_string(null_string) } fn (mut encoder Encoder) encode_array[T](val T) { encoder.output << `[` encoder.open_items(val.len, true) for i, item in val { if i > 0 { encoder.separate_items(true) } encoder.encode_value(item) } encoder.close_items(val.len, true) encoder.output << `]` } @[unsafe] fn (mut encoder Encoder) encode_pointer_array_item[T](item T) { encoder.encode_value(*item) } struct EncoderMapKey[K] { name string key K } @[manualfree] fn (mut encoder Encoder) encode_map[K, T](val map[K]T) { source_keys := val.keys() defer { // Map string keys are cloned; release their text before the array storage. $if K.unaliased_typ is string { for key in source_keys { unsafe { key.free() } } } raw_source_keys := unsafe { &array(&source_keys) } unsafe { raw_source_keys.free() } } mut keys := []EncoderMapKey[K]{cap: val.len} defer { $if K.unaliased_typ is string { // Names borrow the owned source keys. } $else { for i in 0 .. keys.len { unsafe { keys[i].name.free() } } } // Entries borrow source keys; only the nonstring names are released above. raw_keys := unsafe { &array(&keys) } unsafe { raw_keys.free() } } for i in 0 .. source_keys.len { key := &source_keys[i] keys << EncoderMapKey[K]{ name: '${*key}' key: *key } } // Compare the converted object names: integer keys also sort lexically. keys.sort(a.name < b.name) encoder.output << `{` encoder.open_items(val.len, false) for i, entry in keys { if i > 0 { encoder.separate_items(false) } encoder.encode_key(entry.name) encoder.write_key_separator() $if T is $option { // `or` would unwrap the payload and discard a present none-valued entry. encoder.encode_value[T](val[entry.key]) } $else { item := val[entry.key] or { continue } encoder.encode_value[T](item) } } encoder.close_items(val.len, false) encoder.output << `}` } fn (mut encoder Encoder) encode_enum[T](val T) { if encoder.enum_as_int || enum_uses_json_as_number[T]() { encoder.encode_enum_number(val) } else { mut enum_val := 'unknown enum value' $for member in T.values { if member.value == val { enum_val = member.name for attr in member.attrs { if json_attr := json_attr_value(attr) { enum_val = json_attr } } } } encoder.encode_string(enum_val) } } fn (mut encoder Encoder) encode_sumtype[T](val &T) { $if T is $pointer { // Pointer types are handled by encode_value's $pointer branch; // this instantiation is generated but never called. } $else { $for variant in T.variants { if val is variant { // An option variant comes first: the time and struct checks below also // hold for an option of a time or a struct. $if variant.typ is $option { // Like the removed `json` module, a `none` option variant is `{}`. variant_value := val if variant_value == none { encoder.output << `{` encoder.output << `}` } else { encoder.encode_sumtype_option_payload(get_value_from_optional(variant_value)) } } $else $if variant.typ.unaliased_typ is time.Time { // A `time.Time` alias variant (`type Timestamp = time.Time`) is a time too. if T.name in ['x.json2.Any', 'json2.Any', 'Any'] { variant_value := val encoder.encode_value(variant_value) } else { // Like the removed `json` module, every time variant is `Time`, also // an alias such as `type Timestamp = time.Time`. encoder.encode_sumtype_time_variant(time.Time(val), 'Time') } } $else $if variant.typ is $struct { if T.name in ['x.json2.Any', 'json2.Any', 'Any'] { variant_value := val encoder.encode_value(variant_value) } else { // A struct alias variant is tagged with the struct's name, like in the // removed module (`Foo` for `type Alias = Foo`). encoder.encode_sumtype_struct_variant(val, sumtype_variant_name(typeof(variant.typ.unaliased_typ).name)) } } $else $if variant.typ is $map { encoder.encode_value(val) } $else $if variant.typ is $array_dynamic { if T.name in ['x.json2.Any', 'json2.Any', 'Any'] { variant_value := val encoder.encode_value(variant_value) } else { variant_value := val encoder.encode_array_of_sumtype_variants(variant_value) } } $else $if variant.typ is $array_fixed { variant_value := val encoder.encode_fixed_array_of_sumtype_variants(variant_value) } $else { variant_value := val encoder.encode_value(variant_value) } // An alias variant and its base type both match `is` // (`type MyString = string` in `MyString | string`); encode the // value once. return } } } } // encode_sumtype_option_payload writes the value of a set option variant of a sum // type like the variant it holds, as the removed `json` module did: a time (also an // alias of one) as `{"_type":"Time","value":...}`, and a struct with its `_type`, also // as an element of a (nested or fixed size) array. fn (mut encoder Encoder) encode_sumtype_option_payload[P](payload P) { $if P.unaliased_typ is time.Time { encoder.encode_sumtype_time_variant(time.Time(payload), 'Time') } $else $if P.unaliased_typ is $struct { encoder.encode_sumtype_struct_variant(payload, struct_variant_tag[P]()) } $else $if P is $array_dynamic { encoder.encode_array_of_sumtype_variants(payload) } $else $if P is $array_fixed { encoder.encode_fixed_array_of_sumtype_variants(payload) } $else { encoder.encode_value(payload) } } fn (mut encoder Encoder) encode_object_key(is_first bool, key string) bool { if is_first { if encoder.prettify { encoder.increment_level() } } else { encoder.output << `,` } if encoder.prettify { encoder.add_indent() } encoder.encode_key(key) encoder.write_key_separator() return false } fn (mut encoder Encoder) encode_sumtype_struct_variant[T](val T, variant_name string) { $for field in T.fields { $if field.is_embed { unsafe { encoder.encode_sumtype_struct_variant_with_embeds(val, variant_name) } return } } encoder.output << `{` mut is_first := unsafe { encoder.encode_plain_struct_fields[T](val, true) } is_first = encoder.encode_object_key(is_first, '_type') encoder.encode_string(variant_name) encoder.close_object(!is_first) } // encode_array_of_sumtype_variants writes an array held by a sum type variant. Like // the removed `json` module, its struct elements get their `_type`, also in nested // and fixed size arrays. fn (mut encoder Encoder) encode_array_of_sumtype_variants[T](val []T) { encoder.output << `[` encoder.open_items(val.len, true) for i, item in val { if i > 0 { encoder.separate_items(true) } encoder.encode_sumtype_array_item(item) } encoder.close_items(val.len, true) encoder.output << `]` } // encode_fixed_array_of_sumtype_variants writes a fixed size array held by a sum type // variant, laid out like `encode_value` lays out other fixed size arrays. fn (mut encoder Encoder) encode_fixed_array_of_sumtype_variants[A](val A) { encoder.output << `[` // JSON5 and the legacy layout spread fixed size arrays like dynamic arrays. spread := encoder.prettify && (encoder.legacy_layout || encoder.json5) if spread { encoder.open_items(val.len, true) } for i in 0 .. val.len { if i > 0 { if spread { encoder.separate_items(true) } else { encoder.output << `,` } } encoder.encode_sumtype_array_item(val[i]) } if spread || encoder.json5 { encoder.close_items(val.len, true) } encoder.output << `]` } fn (mut encoder Encoder) encode_sumtype_array_item[T](item T) { // An option element comes first, like an option variant: `none` is `{}`, and a set // value is written like the variant it holds, as in the removed module. $if T is $option { if item == none { encoder.output << `{` encoder.output << `}` } else { encoder.encode_sumtype_option_payload(get_value_from_optional(item)) } } $else $if T.unaliased_typ is time.Time { // Like a time variant, `{"_type":"Time","value":...}` as in the removed module. encoder.encode_sumtype_time_variant(time.Time(item), 'Time') } $else $if T is JsonEncoder { encoder.encode_value(item) } $else $if T is Encodable { encoder.encode_value(item) } $else $if T is $struct { encoder.encode_sumtype_struct_variant(item, struct_variant_tag[T]()) } $else $if T is $array_dynamic { encoder.encode_array_of_sumtype_variants(item) } $else $if T is $array_fixed { encoder.encode_fixed_array_of_sumtype_variants(item) } $else { encoder.encode_value(item) } } @[markused] fn (mut encoder Encoder) encode_sumtype_time_variant(val time.Time, variant_name string) { encoder.output << `{` mut is_first := true is_first = encoder.encode_object_key(is_first, '_type') encoder.encode_string(variant_name) is_first = encoder.encode_object_key(is_first, 'value') encoder.encode_number(val.unix()) encoder.close_object(!is_first) } struct EncoderFieldInfo { key_name string // Compact ASCII keys include a leading comma, skipped for the first member. compact_key string is_skip bool is_omitempty bool is_required bool is_json_null bool } struct EncoderFieldInfoCache { mut: field_infos []EncoderFieldInfo } // Keep runtime attribute parsing outside the compile-time field loop. This is called only while // a struct type's field metadata cache is initialized. @[manualfree; noinline] fn encoder_field_info(field_name string, attrs []string) EncoderFieldInfo { mut is_skip := false mut key_name := '' mut is_omitempty := false mut is_required := false mut is_json_null := false for attr in attrs { match attr { 'skip' { is_skip = true break } 'omitempty' { is_omitempty = true } 'required' { is_required = true } 'json_null' { is_json_null = true } else {} } if attr.starts_with('json:') { json_attr := json_attr_value(attr) or { continue } if json_attr == '-' { is_skip = true break } key_name = json_attr } } resolved_key := if key_name == '' { field_name } else { key_name.clone() } compact_key := if !is_skip && next_string_escape(resolved_key, 0, true) == resolved_key.len { ',"' + resolved_key + '":' } else { '' } return EncoderFieldInfo{ key_name: resolved_key compact_key: compact_key is_skip: is_skip is_omitempty: is_omitempty is_required: is_required is_json_null: is_json_null } } fn get_value_from_optional[T](val ?T) T { return val or { T{} } } fn check_not_empty[T](val T) ?bool { $if val is ?string { opt := ?string(val) if sval := opt { return sval != '' } return false } $else $if val is ?bool { opt := ?bool(val) if bval := opt { return bval } return false } $else $if val is ?int { opt := ?int(val) if ival := opt { return ival != 0 } return false } $else $if val is ?f64 { opt := ?f64(val) if fval := opt { return fval != 0.0 } return false } $else $if val is ?f32 { opt := ?f32(val) if fval := opt { return fval != 0.0 } return false } $else $if T is $option { if struct_field_is_none(val) { return false } return check_not_empty(get_value_from_optional(val)) or { true } } $else $if T.indirections != 0 { return val != unsafe { nil } } $else $if T.unaliased_typ is bool { return bool(val) } $else $if T.unaliased_typ is string { if val == '' { return false } } $else $if T.unaliased_typ is $int || T.unaliased_typ is $float { if val == 0 { return false } } $else $if T.unaliased_typ is $array || T.unaliased_typ is $map { return val.len != 0 } $else $if T.unaliased_typ is $enum { return val != unsafe { T(0) } } $else $if T.unaliased_typ is $struct || T.unaliased_typ is $sumtype { // Like the removed `json` module, a struct or sum type value is empty when it // is its type's default value (a struct with its field defaults). return val != T{} } return true } struct EncoderFieldInfoInitialization { mut: initializing u64 initialized u64 } // Only field access and metadata construction depend on T. Keep election and // waiting in one function shared by all DTO types. @[noinline] fn (mut state EncoderFieldInfoInitialization) begin() bool { if stdatomic.fetch_add_u64(&state.initializing, 1) == 0 { return true } for stdatomic.load_u64(&state.initialized) == 0 { time.sleep(time.microsecond) } return false } // TODO: fix compilation with -autofree, and remove the tag @[manualfree] here: @[manualfree; unsafe] fn (mut encoder Encoder) cached_field_infos[T]() &EncoderFieldInfoCache { static cache := &EncoderFieldInfoCache(nil) static state := EncoderFieldInfoInitialization{} // Warm encodes still need only the existing atomic load and branch. if stdatomic.load_u64(&state.initialized) == 0 && state.begin() { cache = &EncoderFieldInfoCache{} $for field in T.fields { cache.field_infos << encoder_field_info(field.name, field.attrs) } // Publish every field before another thread can read the immutable cache. stdatomic.store_u64(&state.initialized, 1) } return cache } fn (mut encoder Encoder) encode_struct_field_value[T](val T) { $if T is $interface { encoder.encode_null() } $else $if T.unaliased_typ is voidptr { encoder.encode_null() } $else $if T.pointee_type is $interface { encoder.encode_null() } $else $if T is $option { if val == none { encoder.output.write_string(null_string) } else { encoder.encode_value(get_value_from_optional(val)) } } $else $if T is $pointer { // encode_value follows the pointer one level at a time, so a nil pointer at // any level (`&&int` pointing to a nil `&int`) is written as `null`. encoder.encode_value(val) } $else { encoder.encode_value(val) } } fn struct_field_is_none[T](val T) bool { $if T is $option { return val == none } return false } fn struct_field_is_nil[T](val T) bool { $if T.indirections != 0 { return val == unsafe { nil } } return false } fn struct_field_should_encode[T](field_info EncoderFieldInfo, val T) bool { if field_info.is_skip { return false } if field_info.is_omitempty { if !(check_not_empty(val) or { false }) { return false } } if !field_info.is_required && !field_info.is_json_null && struct_field_is_none(val) { return false } if struct_field_is_nil(val) { return false } return true } // encode_cached_struct_key copies ordinary compact keys from immutable metadata. // Pretty layouts and keys requiring escaping use the existing encoder. fn (mut encoder Encoder) encode_cached_struct_key(is_first bool, field_info EncoderFieldInfo) bool { if !encoder.prettify && !encoder.json5 && field_info.compact_key.len > 0 { start := if is_first { 1 } else { 0 } // The cached string always contains a comma, quotes, the key, and a colon. unsafe { encoder.output.push_many(field_info.compact_key.str + start, field_info.compact_key.len - start) } return false } return encoder.encode_object_key(is_first, field_info.key_name) } // encode_struct_field_key keeps the non-type-specific part of struct field // encoding out of the comptime field loop. Otherwise every field gets its own // copy of the key-collision scan and key selection code. @[noinline] fn (mut encoder Encoder) encode_struct_field_key(mut used_keys []string, old_used_keys []string, prefix string, field_info EncoderFieldInfo, is_first bool, track_keys bool) bool { if field_info.key_name in old_used_keys { return encoder.encode_object_key(is_first, prefix + field_info.key_name) } if track_keys { used_keys << field_info.key_name } return encoder.encode_cached_struct_key(is_first, field_info) } @[noinline] fn (mut encoder Encoder) encode_embedded_struct_field_key(mut used_keys []string, reserved_keys []string, prefix string, field_info EncoderFieldInfo, is_first bool) bool { should_prefix := field_info.key_name in used_keys || field_info.key_name in reserved_keys if !should_prefix { used_keys << field_info.key_name return encoder.encode_cached_struct_key(is_first, field_info) } return encoder.encode_object_key(is_first, prefix + field_info.key_name) } // encode_struct_field writes a struct field with its key, unless the field is skipped or // left out as empty, and returns the new `is_first`. It is specialized per field type, so // all structs share it and each struct only pays for one call per field. `other_keys` are // the keys of the outer struct for an embedded struct field, else the keys used before. fn (mut encoder Encoder) encode_struct_field[F](val F, field_info EncoderFieldInfo, is_first bool, mut used_keys []string, other_keys []string, prefix string, embedded bool, track_keys bool) bool { if !struct_field_should_encode(field_info, val) { return is_first } new_is_first := if embedded { encoder.encode_embedded_struct_field_key(mut used_keys, other_keys, prefix, field_info, is_first) } else { encoder.encode_struct_field_key(mut used_keys, other_keys, prefix, field_info, is_first, track_keys) } encoder.encode_struct_field_value(val) return new_is_first } @[unsafe] fn (mut encoder Encoder) encode_struct_with_embeds[T](val T) { encoder.output << `{` is_first := encoder.encode_embedded_struct_fields[T](val, true, [], [], '') encoder.close_object(!is_first) } @[unsafe] fn (mut encoder Encoder) encode_sumtype_struct_variant_with_embeds[T](val T, variant_name string) { encoder.output << `{` mut is_first := encoder.encode_embedded_struct_fields[T](val, true, [], [], '') is_first = encoder.encode_object_key(is_first, '_type') encoder.encode_string(variant_name) encoder.close_object(!is_first) } // encode_plain_struct_field does not need embedded-field collision tracking. Its // typed value encoder is shared by all ordinary structs with the same field type. fn (mut encoder Encoder) encode_plain_struct_field[F](val F, field_info EncoderFieldInfo, is_first bool) bool { if !struct_field_should_encode(field_info, val) { return is_first } new_is_first := encoder.encode_cached_struct_key(is_first, field_info) encoder.encode_struct_field_value(val) return new_is_first } @[unsafe] fn (mut encoder Encoder) encode_plain_struct_fields[T](val T, was_first bool) bool { field_info_cache := encoder.cached_field_infos[T]() mut is_first := was_first mut i := 0 $for field in T.fields { if !field.attrs.contains('skip') { $if field.typ is $shared { shared field_value := unsafe { val.$(field.name) } rlock field_value { is_first = encoder.encode_plain_struct_field(field_value, field_info_cache.field_infos[i], is_first) } } $else { is_first = encoder.encode_plain_struct_field(val.$(field.name), field_info_cache.field_infos[i], is_first) } } i++ } return is_first } @[unsafe] fn (mut encoder Encoder) encode_embedded_struct_fields[T](val T, was_first bool, old_used_keys []string, reserved_keys []string, prefix string) bool { field_info_cache := encoder.cached_field_infos[T]() mut is_first := was_first mut used_keys := old_used_keys.clone() mut i := 0 $for field in T.fields { $if field.is_embed { mut child_reserved_keys := reserved_keys.clone() mut reserved_i := 0 $for reserved_field in T.fields { reserved_field_info := field_info_cache.field_infos[reserved_i] $if !reserved_field.is_embed { if !reserved_field_info.is_skip { child_reserved_keys << reserved_field_info.key_name } } reserved_i++ } new_prefix := prefix + field.name + '.' $if field.typ is $shared { shared field_value := unsafe { val.$(field.name) } rlock field_value { is_first = encoder.encode_embedded_struct_fields(field_value, is_first, used_keys, child_reserved_keys, new_prefix) } } $else { is_first = encoder.encode_embedded_struct_fields(val.$(field.name), is_first, used_keys, child_reserved_keys, new_prefix) } } $else { if !field.attrs.contains('skip') { $if field.typ is $shared { shared field_value := unsafe { val.$(field.name) } rlock field_value { is_first = encoder.encode_struct_field(field_value, field_info_cache.field_infos[i], is_first, mut used_keys, reserved_keys, prefix, true, true) } } $else { is_first = encoder.encode_struct_field(val.$(field.name), field_info_cache.field_infos[i], is_first, mut used_keys, reserved_keys, prefix, true, true) } } } i++ } return is_first } fn (mut encoder Encoder) increment_level() { encoder.level++ encoder.prefix = encoder.line_prefix() } fn (mut encoder Encoder) decrement_level() { encoder.level-- encoder.prefix = encoder.line_prefix() } // line_prefix is the line break and indentation before a member or element at the // current level. fn (encoder &Encoder) line_prefix() string { if encoder.legacy_layout { return '\n' + '\t'.repeat(encoder.level) } return encoder.newline_string + encoder.indent_string.repeat(encoder.level) } // open_items starts the members of an object or the elements of an array, after its // `{` or `[`. Without members or elements no level is raised: its lowering happens // in close_items only for a container that has some. fn (mut encoder Encoder) open_items(count int, is_array bool) { if encoder.prettify && count > 0 { encoder.increment_level() if !(is_array && encoder.legacy_layout) { encoder.add_indent() } } } // separate_items writes the separator between two members or elements. fn (mut encoder Encoder) separate_items(is_array bool) { encoder.output << `,` if encoder.prettify { if is_array && encoder.legacy_layout { encoder.output << ` ` } else { encoder.add_indent() } } } // close_items ends the members or elements started by open_items, before the `}` // or `]`. fn (mut encoder Encoder) close_items(count int, is_array bool) { if encoder.json5 && encoder.trailing_commas && count > 0 { encoder.output << `,` } if !encoder.prettify { return } if count > 0 { encoder.decrement_level() if !(is_array && encoder.legacy_layout) { encoder.add_indent() } } else if !is_array && encoder.legacy_layout { // The removed module wrote an empty object as `{`, a line break and `}`. prefix := encoder.line_prefix() encoder.output.write_string(prefix) } } // close_object ends an object whose members were started with encode_object_key. fn (mut encoder Encoder) close_object(has_members bool) { encoder.close_items(if has_members { 1 } else { 0 }, false) encoder.output << `}` } fn (mut encoder Encoder) write_key_separator() { encoder.output << `:` if encoder.prettify { encoder.output << if encoder.legacy_layout { `\t` } else { ` ` } } } fn (mut encoder Encoder) add_indent() { encoder.output.write_string(encoder.prefix) }