package fejkdata import ( "errors" "fmt" "math" "reflect" "slices" "strconv" "strings" "github.com/larvit/fejkdata/internal/drawstate" "github.com/larvit/fejkdata/internal/grammar" "github.com/larvit/fejkdata/internal/invariant" "github.com/larvit/fejkdata/internal/jsonvalue" "github.com/larvit/fejkdata/internal/proven" ) // FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a // column of one record: its tag a path or an inline template, told apart as [IsTemplate] // tells them, and its Go type the column's datatype. The fields an embedded struct promotes // are columns of that record too, while a named struct field, or a pointer to one, fills // from its own tags as a record of its own. The first call for a type compiles its tags, // so a later call for that type fails only as the first did. func (f *Generator) FakeStruct(v any) error { p := reflect.ValueOf(v) if p.Kind() != reflect.Pointer || p.IsNil() || p.Elem().Kind() != reflect.Struct { return fmt.Errorf("fejkdata: FakeStruct fills a struct through a non-nil pointer, got %T", v) } f.mu.Lock() defer f.mu.Unlock() shape, err := f.structShapeOf(p.Elem().Type()) if err != nil { return fmt.Errorf("fejkdata: %w", withDefaultModules(err, f.defaultModules)) } shape.fill(f.drawState, p.Elem()) return nil } type structResult struct { shape *structShape err error } // maxStructs caps the structs compiling one type walks through its fields. const maxStructs = 1 << 10 // structShapeOf compiles a struct type once and remembers the answer. Callers hold the // generator's lock. func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) { if r, done := f.structs[t]; done { return r.shape, r.err } label := t.Name() if label == "" { label = "struct" } sc := &structCompile{root: &f.root, visiting: map[reflect.Type]bool{}, structsLeft: maxStructs} shape, err := sc.compileShape(t, label) if f.structs == nil { f.structs = map[reflect.Type]structResult{} } f.structs[t] = structResult{shape, err} return shape, err } // structShape is a struct type compiled to fill: its tagged fields as one record, the field // index path each column fills, and the struct fields carrying tags of their own. type structShape struct { record *template fieldIndexes [][]int nested []nestedStruct } // nestedStruct is a named struct field, or a pointer to one, filled as a record of its own. type nestedStruct struct { fieldIndex []int shape *structShape } func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 } // structCompile is what compiling one struct type shares across the structs it reaches: the // loaded tree, the types compiling or embedded above, so a pointer back to one is left alone // rather than filled without end, and how many more structs it may walk. type structCompile struct { root *folder visiting map[reflect.Type]bool structsLeft int } // structFields gathers what one struct type fills: its tagged fields, those its embedded // structs promote included, as the tags of one record, and its named struct fields as nested // records. type structFields struct { *structCompile typ reflect.Type label string tags map[string]jsonvalue.Value shape *structShape } func (sc *structCompile) compileShape(t reflect.Type, label string) (*structShape, error) { if err := sc.spend(label); err != nil { return nil, err } sc.visiting[t] = true defer delete(sc.visiting, t) c := &structFields{structCompile: sc, typ: t, label: label, tags: map[string]jsonvalue.Value{}, shape: &structShape{}} if err := c.gatherFields(t, nil); err != nil { return nil, err } if len(c.tags) > 0 { if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil { return nil, err } } return c.shape, nil } func (sc *structCompile) spend(label string) error { if sc.structsLeft--; sc.structsLeft >= 0 { return nil } return fmt.Errorf(`%s: the struct fields reach more than %d structs; leave a struct field unfilled with fake:"-"`, label, maxStructs) } // index is the field index path, as reflect's FieldByIndex takes, from c.typ to t. func (c *structFields) gatherFields(t reflect.Type, index []int) error { for i := 0; i < t.NumField(); i++ { sf := t.Field(i) sf.Index = append(index[:len(index):len(index)], i) if err := c.gatherField(sf); err != nil { return err } } return nil } func (c *structFields) gatherField(sf reflect.StructField) error { tag, tagged := sf.Tag.Lookup("fake") elem := structOf(sf.Type) switch { case tagged && tag == "-": return nil case tagged: return c.addTag(sf, tag) case elem == nil || c.visiting[elem]: return nil case sf.Anonymous: return c.embed(sf, elem) case sf.IsExported(): return c.nest(sf, elem) } return nil } // structOf is the struct type a field holds, by value or through a pointer; nil when none. func structOf(t reflect.Type) reflect.Type { if t.Kind() == reflect.Pointer { t = t.Elem() } if t.Kind() != reflect.Struct { return nil } return t } // addTag adds a tagged field's tag to the record, refusing one another field hides. func (c *structFields) addTag(sf reflect.StructField, tag string) error { if visible, ok := c.typ.FieldByName(sf.Name); !ok || !slices.Equal(visible.Index, sf.Index) { return fmt.Errorf("%s.%s: hidden by another field named %s, so its fake tag cannot fill it; rename one", c.label, fieldPath(c.typ, sf.Index), sf.Name) } v, err := tagValue(sf, tag) if err != nil { return fmt.Errorf("%s.%s: %w", c.label, sf.Name, err) } c.tags[sf.Name] = v return nil } // fieldPath names the field at index within t through each struct it is embedded in. func fieldPath(t reflect.Type, index []int) string { names := make([]string, len(index)) for i := range index { names[i] = t.FieldByIndex(index[:i+1]).Name } return strings.Join(names, ".") } func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error { if err := c.spend(c.label + "." + fieldPath(c.typ, sf.Index)); err != nil { return err } c.visiting[elem] = true defer delete(c.visiting, elem) tags, nested := len(c.tags), len(c.shape.nested) if err := c.gatherFields(elem, sf.Index); err != nil { return err } if sf.Type.Kind() == reflect.Pointer && !sf.IsExported() && (len(c.tags) > tags || len(c.shape.nested) > nested) { return fmt.Errorf("%s.%s: an unexported embedded pointer field cannot be set, so the tags beneath it cannot fill; embed %s by value", c.label, fieldPath(c.typ, sf.Index), elem) } return nil } func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error { nested, err := c.compileShape(elem, c.label+"."+sf.Name) if err != nil || nested.empty() { return err } c.shape.nested = append(c.shape.nested, nestedStruct{sf.Index, nested}) return nil } // tagValue reads a field's fake tag as the value its column compiles from: an inline template // as written, or a path as the reference {/path}. func tagValue(sf reflect.StructField, tag string) (jsonvalue.Value, error) { if err := checkTaggedType(sf); err != nil { return jsonvalue.Value{}, err } inline, err := grammar.IsTemplate(tag) switch { case err != nil: return jsonvalue.Value{}, err case inline: return inputValue(tag), nil } path, err := grammar.CallerPath(tag) if err != nil { return jsonvalue.Value{}, err } if err := grammar.CheckPathIdentifiers(path); err != nil { return jsonvalue.Value{}, err } return jsonvalue.String("{/" + path + "}"), nil } // checkTaggedType rejects a tagged field no column can fill. func checkTaggedType(sf reflect.StructField) error { _, holds := columnKinds[sf.Type.Kind()] if sf.Type.Kind() == reflect.Pointer { _, holds = columnKinds[sf.Type.Elem().Kind()] } switch { case !sf.IsExported(): return errors.New("unexported, so its fake tag cannot fill it") case holds: return nil case structOf(sf.Type) != nil: return errors.New(`a struct field fills from the tags on its own fields; drop this one, or write fake:"-" to leave it unfilled`) } return fmt.Errorf("a fake tag fills a string, bool, integer or float field, or a pointer to one, not %s", sf.Type) } // compileRecord compiles the tagged fields of t as one record, and proves each column holds // only what its field's Go type can. func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]jsonvalue.Value) error { tags["format"] = jsonvalue.String("") n, err := compileFrom(jsonvalue.Object(tags), atGoTop) if err != nil { return fmt.Errorf("%s: %w", label, err) } if err := loadInline(root, n, label, true); err != nil { return err } record, err := recordOf(n) if err != nil { return fmt.Errorf("%s: %w", label, err) } proof := &valueProof{} s.fieldIndexes = make([][]int, len(record.columns)) for i, c := range record.columns { sf, _ := t.FieldByName(c.name) if err := proof.checkField(label+"."+c.name, sf.Type, c.field); err != nil { return err } s.fieldIndexes[i] = sf.Index } s.record = record return nil } // columnKind is what a field of one Go kind holds: the datatype its text proves as, the range a // number of it stays in, and the kind to name when a value is not proven within that range. type columnKind struct { datatype DataType lo, hi float64 wider reflect.Kind } var columnKinds = map[reflect.Kind]columnKind{ reflect.Bool: {datatype: DataTypeBoolean}, reflect.Float32: {DataTypeNumber, -math.MaxFloat32, math.MaxFloat32, reflect.Float64}, reflect.Float64: {DataTypeNumber, -math.MaxFloat64, math.MaxFloat64, reflect.Float64}, reflect.Int: {DataTypeInteger, math.MinInt, math.MaxInt, reflect.Int64}, reflect.Int16: {DataTypeInteger, math.MinInt16, math.MaxInt16, reflect.Int64}, reflect.Int32: {DataTypeInteger, math.MinInt32, math.MaxInt32, reflect.Int64}, reflect.Int64: {DataTypeInteger, math.MinInt64, math.MaxInt64, reflect.Int64}, reflect.Int8: {DataTypeInteger, math.MinInt8, math.MaxInt8, reflect.Int64}, reflect.String: {datatype: DataTypeString}, reflect.Uint: {DataTypeInteger, 0, math.MaxUint, reflect.Int64}, reflect.Uint16: {DataTypeInteger, 0, math.MaxUint16, reflect.Int64}, reflect.Uint32: {DataTypeInteger, 0, math.MaxUint32, reflect.Int64}, reflect.Uint64: {DataTypeInteger, 0, math.MaxUint64, reflect.Int64}, reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8, reflect.Int64}, } // holds reports whether a field of this kind holds every value v proves. An integer prints // whole, so its bounds round inward first. func (k columnKind) holds(v proven.Facts) bool { switch k.datatype { case DataTypeString, DataTypeBoolean: return true case DataTypeInteger: return math.Ceil(v.Lo) >= k.lo && math.Floor(v.Hi) <= k.hi } return v.Lo >= k.lo && v.Hi <= k.hi } // fill draws the record into v's tagged fields, then each nested struct as a record of its own. func (s *structShape) fill(state *drawstate.State, v reflect.Value) { if s.record != nil { var frames frameStack for i, c := range renderRecord(state, s.record, renderEnv{frames: &frames}).columns { setColumn(fieldAt(v, s.fieldIndexes[i]), c) } } for _, n := range s.nested { field := fieldAt(v, n.fieldIndex) if field.Kind() == reflect.Pointer { if field.IsNil() { field.Set(reflect.New(field.Type().Elem())) } field = field.Elem() } n.shape.fill(state, field) } } // fieldAt is v's field at index, allocating each nil embedded pointer on the way. func fieldAt(v reflect.Value, index []int) reflect.Value { for _, i := range index { if v.Kind() == reflect.Pointer { if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } v = v.Elem() } v = v.Field(i) } return v } // setColumn writes a drawn column into its field: a null as a nil pointer, a value through a // fresh pointer or straight into the field. func setColumn(field reflect.Value, c Column) { if field.Kind() != reflect.Pointer { setText(field, c.Value) return } if c.Null { field.SetZero() return } value := reflect.New(field.Type().Elem()) setText(value.Elem(), c.Value) field.Set(value) } // setText parses text into a field of one of columnKinds, which checkField proved it parses as. func setText(field reflect.Value, text string) { var err error switch kind := columnKinds[field.Kind()]; { case kind.datatype == DataTypeString: field.SetString(text) case kind.datatype == DataTypeBoolean: var b bool b, err = strconv.ParseBool(text) field.SetBool(b) case kind.datatype == DataTypeNumber: var x float64 x, err = strconv.ParseFloat(text, field.Type().Bits()) field.SetFloat(x) case field.CanInt(): var n int64 n, err = strconv.ParseInt(text, 10, field.Type().Bits()) field.SetInt(n) default: var n uint64 n, err = strconv.ParseUint(text, 10, field.Type().Bits()) field.SetUint(n) } if err != nil { panic(invariant.Broken("%q reached a %s field unproven: %v", text, field.Type(), err)) } }