package core import ( "database/sql/driver" "encoding/json" "fmt" "slices" "strconv" ) // NewFieldsList creates a new FieldsList instance with the provided fields. func NewFieldsList(fields ...Field) FieldsList { l := make(FieldsList, 0, len(fields)) for _, f := range fields { l.add(-1, f) } return l } // FieldsList defines a Collection slice of fields. type FieldsList []Field // Clone creates a deep clone of the current list. func (l FieldsList) Clone() (FieldsList, error) { copyRaw, err := json.Marshal(l) if err != nil { return nil, err } result := FieldsList{} if err := json.Unmarshal(copyRaw, &result); err != nil { return nil, err } return result, nil } // FieldNames returns a slice with the name of all list fields. func (l FieldsList) FieldNames() []string { result := make([]string, len(l)) for i, field := range l { result[i] = field.GetName() } return result } // AsMap returns a map with all registered list field. // The returned map is indexed with each field name. func (l FieldsList) AsMap() map[string]Field { result := make(map[string]Field, len(l)) for _, field := range l { result[field.GetName()] = field } return result } // GetById returns a single field by its id. func (l FieldsList) GetById(fieldId string) Field { for _, field := range l { if field.GetId() == fieldId { return field } } return nil } // GetByName returns a single field by its name. func (l FieldsList) GetByName(fieldName string) Field { for _, field := range l { if field.GetName() == fieldName { return field } } return nil } // RemoveById removes a single field by its id. // // This method does nothing if field with the specified id doesn't exist. func (l *FieldsList) RemoveById(fieldId string) { fields := *l for i, field := range fields { if field.GetId() == fieldId { *l = append(fields[:i], fields[i+1:]...) return } } } // RemoveByName removes a single field by its name. // // This method does nothing if field with the specified name doesn't exist. func (l *FieldsList) RemoveByName(fieldName string) { fields := *l for i, field := range fields { if field.GetName() == fieldName { *l = append(fields[:i], fields[i+1:]...) return } } } // Add adds one or more fields to the current list. // // By default this method will try to REPLACE existing fields with // the new ones by their id or by their name if the new field doesn't have an explicit id. // // If no matching existing field is found, it will APPEND the field to the end of the list. // // In all cases, if any of the new fields don't have an explicit id it will auto generate a default one for them // (the id value doesn't really matter and it is mostly used as a stable identifier in case of a field rename). func (l *FieldsList) Add(fields ...Field) { for _, f := range fields { l.add(-1, f) } } // AddAt is the same as Add but insert/move the fields at the specific position. // // If pos < 0, then this method acts the same as calling Add. // // If pos > FieldsList total items, then the specified fields are inserted/moved at the end of the list. func (l *FieldsList) AddAt(pos int, fields ...Field) { total := len(*l) for i, f := range fields { if pos < 0 { l.add(-1, f) } else if pos > total { l.add(total+i, f) } else { l.add(pos+i, f) } } } // AddMarshaledJSON parses the provided raw json data and adds the // found fields into the current list (following the same rule as the Add method). // // The rawJSON argument could be one of: // - serialized array of field objects // - single field object. // // Example: // // l.AddMarshaledJSON([]byte{`{"type":"text", name: "test"}`}) // l.AddMarshaledJSON([]byte{`[{"type":"text", name: "test1"}, {"type":"text", name: "test2"}]`}) func (l *FieldsList) AddMarshaledJSON(rawJSON []byte) error { extractedFields, err := marshaledJSONtoFieldsList(rawJSON) if err != nil { return err } l.Add(extractedFields...) return nil } // AddMarshaledJSONAt is the same as AddMarshaledJSON but insert/move the fields at the specific position. // // If pos < 0, then this method acts the same as calling AddMarshaledJSON. // // If pos > FieldsList total items, then the specified fields are inserted/moved at the end of the list. func (l *FieldsList) AddMarshaledJSONAt(pos int, rawJSON []byte) error { extractedFields, err := marshaledJSONtoFieldsList(rawJSON) if err != nil { return err } l.AddAt(pos, extractedFields...) return nil } func marshaledJSONtoFieldsList(rawJSON []byte) (FieldsList, error) { extractedFields := FieldsList{} // nothing to add if len(rawJSON) == 0 { return extractedFields, nil } // try to unmarshal first into a new fieds list // (assuming that rawJSON is array of objects) err := json.Unmarshal(rawJSON, &extractedFields) if err != nil { // try again but wrap the rawJSON in [] // (assuming that rawJSON is a single object) wrapped := make([]byte, 0, len(rawJSON)+2) wrapped = append(wrapped, '[') wrapped = append(wrapped, rawJSON...) wrapped = append(wrapped, ']') err = json.Unmarshal(wrapped, &extractedFields) if err != nil { return nil, fmt.Errorf("failed to unmarshal the provided JSON - expects array of objects or just single object: %w", err) } } return extractedFields, nil } func (l *FieldsList) add(pos int, newField Field) { fields := *l var replaceByName bool var replaceInPlace bool if pos < 0 { replaceInPlace = true pos = len(fields) } else if pos > len(fields) { pos = len(fields) } newFieldId := newField.GetId() // set default id if newFieldId == "" { replaceByName = true baseId := newField.Type() + crc32Checksum(newField.GetName()) newFieldId = baseId for i := 2; i < 1000; i++ { if l.GetById(newFieldId) == nil { break // already unique } newFieldId = baseId + strconv.Itoa(i) } newField.SetId(newFieldId) } // try to replace existing for i, field := range fields { if replaceByName { if name := newField.GetName(); name != "" && field.GetName() == name { // reuse the original id newField.SetId(field.GetId()) if replaceInPlace { (*l)[i] = newField return } else { // remove the current field and insert it later at the specific position *l = slices.Delete(*l, i, i+1) if total := len(*l); pos > total { pos = total } break } } } else { if field.GetId() == newFieldId { if replaceInPlace { (*l)[i] = newField return } else { // remove the current field and insert it later at the specific position *l = slices.Delete(*l, i, i+1) if total := len(*l); pos > total { pos = total } break } } } } // insert the new field *l = slices.Insert(*l, pos, newField) } // String returns the string representation of the current list. func (l FieldsList) String() string { v, _ := json.Marshal(l) return string(v) } type onlyFieldType struct { Type string `json:"type"` } type fieldWithType struct { Field Type string `json:"type"` } func (fwt *fieldWithType) UnmarshalJSON(data []byte) error { // extract the field type to init a blank factory t := &onlyFieldType{} if err := json.Unmarshal(data, t); err != nil { return fmt.Errorf("failed to unmarshal field type: %w", err) } factory, ok := Fields[t.Type] if !ok { return fmt.Errorf("missing or unknown field type in %s", data) } fwt.Type = t.Type fwt.Field = factory() // unmarshal the rest of the data into the created field if err := json.Unmarshal(data, fwt.Field); err != nil { return fmt.Errorf("failed to unmarshal field: %w", err) } return nil } // UnmarshalJSON implements [json.Unmarshaler] and // loads the provided json data into the current FieldsList. func (l *FieldsList) UnmarshalJSON(data []byte) error { fwts := []fieldWithType{} if err := json.Unmarshal(data, &fwts); err != nil { return err } *l = []Field{} // reset for _, fwt := range fwts { l.add(-1, fwt.Field) } return nil } // MarshalJSON implements the [json.Marshaler] interface. func (l FieldsList) MarshalJSON() ([]byte, error) { if l == nil { l = []Field{} // always init to ensure that it is serialized as empty array } wrapper := make([]map[string]any, 0, len(l)) for _, f := range l { // precompute the json into a map so that we can append the type to a flatten object raw, err := json.Marshal(f) if err != nil { return nil, err } data := map[string]any{} if err := json.Unmarshal(raw, &data); err != nil { return nil, err } data["type"] = f.Type() wrapper = append(wrapper, data) } return json.Marshal(wrapper) } // Value implements the [driver.Valuer] interface. func (l FieldsList) Value() (driver.Value, error) { data, err := json.Marshal(l) return string(data), err } // Scan implements [sql.Scanner] interface to scan the provided value // into the current FieldsList instance. func (l *FieldsList) Scan(value any) error { var data []byte switch v := value.(type) { case nil: // no cast needed case []byte: data = v case string: data = []byte(v) default: return fmt.Errorf("failed to unmarshal FieldsList value %q", value) } if len(data) == 0 { data = []byte("[]") } return l.UnmarshalJSON(data) }