Ctrl K
ComunidadGuías…

vlib/json2/encode.v

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
}