diff --git a/collection/json/convert.go b/collection/json/convert.go new file mode 100644 index 0000000..c850234 --- /dev/null +++ b/collection/json/convert.go @@ -0,0 +1,106 @@ +package json + +import ( + j "encoding/json" + "io" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +// JsonDecoder permet de parser une entrée brute en un objet Json ou un tableau de Json. +type JsonDecoder struct { + d *j.Decoder +} + +// NewJsonDecoder retourne un décodeur pour la donnée d’entrée spécifiée. +func NewJsonDecoder(r io.Reader) *JsonDecoder { return &JsonDecoder{d: j.NewDecoder(r)} } + +// Decode retourne un objet Json décodé. +func (dec *JsonDecoder) Decode() Result[Json] { + o := make(Json) + if err := dec.d.Decode(&o); err != nil { + return Err[Json](err) + } + return Ok(o) +} + +// DecodeSlice retourne un tableau de Json décodé. +func (dec *JsonDecoder) DecodeSlice() Result[Slice] { + var sl Slice + if err := dec.d.Decode(&sl); err != nil { + return Err[Slice](err) + } + return Ok(sl) +} + +// Decode décode une entrée brute et la fusionne avec l’objet Json. +func (o Json) Decode(r io.Reader) Result[Json] { + dec := NewJsonDecoder(r) + result := dec.Decode() + + if o2, ok := result.Ok(); ok { + o = o.Fusion(o2) + return Ok(o) + } + return result +} + +// Decode décode une entrée brute et la fusionne avec le tableau de Json. +func (sl *Slice) Decode(r io.Reader) Result[Slice] { + dec := NewJsonDecoder(r) + result := dec.DecodeSlice() + + if sl2, ok := result.Ok(); ok { + return Ok(sl.Add(sl2...)) + } + return result +} + +// JsonEncoder permet de convertir un objet Json ou un tableau de Json au format brut. +type JsonEncoder struct { + e *j.Encoder +} + +// NewJsonEncoder fournit un encodeur pour la sortie fournie. +func NewJsonEncoder(w io.Writer) *JsonEncoder { return &JsonEncoder{e: j.NewEncoder(w)} } + +// Encode encode un objet Json. +func (enc *JsonEncoder) Encode(o Json) error { return enc.e.Encode(o) } + +// EncodeSlice encode un tableau Json. +func (enc *JsonEncoder) EncodeSlice(sl Slice) error { return enc.e.Encode(sl) } + +// SetEscapeHTML configure l’encodeur pour que les caractère HTML soient échappés si on est vrai. +// Ainsi, les caractères &, <, et > sont transformés respectivement en \u0026, \u003c et \u003e +// Cela permet d’éviter des problèmes de sécurité lorsque du html est embarqué dans du Json. +func (enc *JsonEncoder) SetEscapeHTML(on bool) { enc.e.SetEscapeHTML(on) } + +// SetIndent définit le préfixe et l’indentation du Json à la sortie. +// Cela permet de rendre un Json dans un format lisible (ie. non minifié). +func (enc *JsonEncoder) SetIndent(prefix, indent string) { enc.e.SetIndent(prefix, indent) } + +// Encode minifie le Json dans la sortie donnée. +func (o Json) Encode(w io.Writer) error { + enc := NewJsonEncoder(w) + return enc.Encode(o) +} + +// EncodeHuman encode le Json dans un format humainement lisible (indentation de deux espaces). +func (o Json) EncodeHuman(w io.Writer) error { + enc := NewJsonEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(o) +} + +// Encode minifie le tableau de Json dans la sortie donnée. +func (sl Slice) Encode(w io.Writer) error { + enc := NewJsonEncoder(w) + return enc.EncodeSlice(sl) +} + +// EncodeHuman encode le tableau de Json dans un format humainement lisible (indentation de deux espaces). +func (sl Slice) EncodeHuman(w io.Writer) error { + enc := NewJsonEncoder(w) + enc.SetIndent("", " ") + return enc.EncodeSlice(sl) +} diff --git a/collection/json/json.go b/collection/json/json.go new file mode 100644 index 0000000..2fe3ba8 --- /dev/null +++ b/collection/json/json.go @@ -0,0 +1,548 @@ +package json + +import ( + "errors" + "io" + "sort" + "strconv" + "strings" + + "gitea.zaclys.com/bvaudour/gob/collection" + "gitea.zaclys.com/bvaudour/gob/convert" + . "gitea.zaclys.com/bvaudour/gob/option" +) + +var ( + ErrFailedToParse = errors.New("failed to parse") +) + +func isSlice(c convert.Value) bool { + return c.Is(convert.Slice) +} + +func isMap(c convert.Value) bool { + return c.Is(convert.Map) && c.Type().Key().Is(convert.String) +} + +func isAssignableIn(v1, v2 convert.Value) bool { + return v1.Type().AssignableTo(v2.TypeElem()) +} + +func searchSlice(c convert.Value, k string) (out Option[convert.Value]) { + i, err := strconv.Atoi(k) + if err == nil && i >= 0 && i < c.Len() { + out = Some(c.Index(i)) + } + return +} + +func searchMap(c convert.Value, k string) (out Option[convert.Value]) { + return c.MapIndexIfExists(convert.ValueOf(k)) +} + +func search(c convert.Value, k string) (v Option[convert.Value]) { + if isSlice(c) { + v = searchSlice(c, k) + } else if isMap(c) { + v = searchMap(c, k) + } + return +} + +func searchAll(c any, keys []string) (v Option[any]) { + vv := convert.ValueOf(c) + var ok bool + for _, k := range keys { + if vv, ok = search(vv, k).Get(); !ok { + return + } + } + return Some(vv.Interface()) +} + +func setSlice(c convert.Value, v any, k string) (out Option[convert.Value]) { + i, err := strconv.Atoi(k) + vv := convert.ValueOf(v) + l := c.Len() + if err != nil && i >= 0 && i <= l && isAssignableIn(vv, c) { + if i < l { + c.SetIndex(i, vv) + out = Some(c) + } else { + out = Some(convert.Append(c, vv)) + } + } + return +} + +func setMap(c convert.Value, v any, k string) (out Option[convert.Value]) { + vv := convert.ValueOf(v) + if isAssignableIn(vv, c) { + c.SetMapIndex(convert.ValueOf(k), vv) + out = Some(c) + } + return +} + +func set(c convert.Value, v any, k string) (out Option[convert.Value]) { + if isSlice(c) { + return setSlice(c, v, k) + } else if isMap(c) { + return setMap(c, v, k) + } + return +} + +func setAll(c, v any, keys []string) (ok bool) { + l := len(keys) + k1, k2 := keys[l-2], keys[l-1] + cc := convert.ValueOf(c) + for _, k := range keys[:l-2] { + if cc, ok = search(cc, k).Get(); !ok { + return + } + } + var ccl convert.Value + if ccl, ok = search(cc, k1).Get(); ok { + if ccl, ok = set(ccl, v, k2).Get(); ok { + set(cc, ccl.Interface(), k1) + } + } + return +} + +func delSlice(c convert.Value, k string) (out Option[convert.Value]) { + i, err := strconv.Atoi(k) + l := c.Len() + if err != nil && i >= 0 && i < l { + v := convert.SliceOf(c.Type(), l-1, l-1) + convert.Copy(v.Slice(0, i), c.Slice(0, i)) + convert.Copy(v.Slice(i, l-1), c.Slice(i+1, l)) + return Some(v) + } + return +} + +func delMap(c convert.Value, k string) (out Option[convert.Value]) { + kk := convert.ValueOf(k) + if ok := c.MapIndexIfExists(kk).IsDefined(); ok { + c.DelMapIndex(kk) + out = Some(c) + } + return +} + +func del(c convert.Value, k string) (out Option[convert.Value]) { + if isSlice(c) { + return delSlice(c, k) + } else if isMap(c) { + return delMap(c, k) + } + return +} + +func delAll(c any, keys []string) (ok bool) { + l := len(keys) + k1, k2 := keys[l-2], keys[l-1] + cc := convert.ValueOf(c) + for _, k := range keys[:l-2] { + if cc, ok = search(cc, k).Get(); !ok { + return + } + } + var ccl convert.Value + if ccl, ok = search(cc, k1).Get(); ok { + if ccl, ok = del(ccl, k2).Get(); ok { + set(cc, ccl.Interface(), k1) + } + } + return +} + +func flat(v convert.Value, prefix ...string) (keys [][]string, values []any) { + if isSlice(v) { + it, l := v.SliceRange(), len(prefix) + for it.Next() { + k := make([]string, l+1) + copy(k[:l], prefix) + k[l] = strconv.Itoa(it.Index()) + kk, vv := flat(it.Value(), k...) + collection.Add(&keys, kk...) + collection.Add(&values, vv...) + } + } else if isMap(v) { + it, l := v.MapRange(), len(prefix) + for it.Next() { + k := make([]string, l+1) + copy(k[:l], prefix) + k[l] = it.Key().String() + kk, vv := flat(it.Value(), k...) + collection.Add(&keys, kk...) + collection.Add(&values, vv...) + } + } else { + collection.Add(&keys, prefix) + collection.Add(&values, v.Interface()) + } + return +} + +func createRec(o Json, keys []string) Json { + e := o + for _, k := range keys { + v, ok := o[k] + if ok { + if m, ok := v.(Json); ok { + e = m + continue + } + } + m := make(Json) + e[k] = m + e = m + } + return o +} + +func unflat(v convert.Value, split SplitFunc) (out convert.Value) { + if isSlice(v) { + it := v.SliceRange() + for it.Next() { + i, e := it.Index(), unflat(it.Value(), split) + v.SetIndex(i, e) + } + return v + } else if isMap(v) { + result := make(Json) + it := v.MapRange() + for it.Next() { + k, e := it.Key().String(), unflat(it.Value(), split) + spl := SplitKey(k, split) + if keys, ok := spl.Ok(); ok { + result = createRec(result, keys) + result.SetRecursive(e.Interface(), keys...) + } else { + result[k] = e.Interface() + } + } + return convert.ValueOf(result) + } + return v +} + +func sSlice(c convert.Value) convert.Value { + it := c.SliceRange() + for it.Next() { + i, v := it.Index(), slicify(it.Value()) + c.SetIndex(i, v) + } + return c +} + +func canSlice(c convert.Value) (out Option[map[int]string]) { + if !isMap(c) { + return + } + var keys []int + skeys := make(map[int]string) + it := c.MapRange() + for it.Next() { + k := it.Key().String() + i, err := strconv.Atoi(k) + if err != nil || i < 0 { + return + } + collection.Add(&keys, i) + skeys[i] = k + } + sort.Ints(keys) + for i, j := range keys { + if i != j { + return + } + } + return Some(skeys) +} + +func sMap(c convert.Value) convert.Value { + if keys, ok := canSlice(c).Get(); ok { + sl := convert.SliceOf(convert.SliceTypeOf(c.TypeElem()), len(keys), len(keys)) + it := sl.SliceRange() + for it.Next() { + i := it.Index() + k := keys[i] + v := slicify(c.MapIndex(convert.ValueOf(k))) + sl.SetIndex(i, v) + } + return sl + } + it := c.MapRange() + for it.Next() { + k, v := it.Key(), slicify(it.Value()) + c.SetMapIndex(k, v) + } + return c +} + +func slicify(v convert.Value) convert.Value { + if isSlice(v) { + return sSlice(v) + } else if isMap(v) { + return sMap(v) + } + return v +} + +// Json représente un simple objet JSON. +type Json map[string]any + +// SplitFunc est une fonction qui découpe une clé en sous-clés. +// +// Paramètres : +// - key : la clé à découper +// +// Sortie : +// - current : la sous-clé parsée +// - next : la partie de clé qui reste à parser +// - err : une erreur retournée lors du parsage ou io.EOF si le parsage est terminé +type SplitFunc func(key string) (current, next string, err error) + +// MergeFunc est la transformée inverse de SplitFunc. +type MergeFunc func(keys []string) (key string) + +// SplitDot découpe une clé qui est sous la forme a.b.c… +func SplitDot(key string) (current, next string, err error) { + i := strings.Index(key, ".") + if i < 0 { + current, err = key, io.EOF + } else { + current, next = key[:i], key[i+1:] + } + + return +} + +// SplitBracket découpe une clé qui est sous la forme a[b][c]… +func SplitBracket(key string) (current, next string, err error) { + i0 := strings.Index(key, "[") + i1 := strings.Index(key, "]") + if i0 < 0 { + if i1 >= 0 { + err = ErrFailedToParse + } else { + current, err = key, io.EOF + } + } else if i0 >= i1 || (len(key) > i1+1 && key[i1+1] != '[') { + err = ErrFailedToParse + } else { + current, next = key[:i0], key[i0+1:i1]+key[i1:] + } + + return +} + +// SplitKey découpe la clé selon la fonction de découpage fournie. +func SplitKey(key string, split SplitFunc) (out Result[[]string]) { + current, next := key, "" + var err error + var keys []string + for current, next, err = split(current); err == nil; current, next, err = split(current) { + collection.Add(&keys, current) + current = next + } + + if err == io.EOF { + return Ok(keys) + } + return Err[[]string](err) +} + +// MergeDot crée une clé sous la forme a.b.c… +func MergeDot(keys []string) string { + return strings.Join(keys, ".") +} + +// MergeBracket crée une clé sous la forme a[b][c]… +func MergeBracket(keys []string) (key string) { + if len(keys) == 0 { + return + } + key, keys = keys[0], keys[1:] + for _, k := range keys { + key += "[" + k + "]" + } + + return +} + +func (o Json) get(keys ...string) (v Option[any]) { + switch len(keys) { + case 0: + v = Some[any](o) + case 1: + v = Some[any](o[keys[0]]) + default: + v = searchAll(o, keys) + } + return +} + +// Get retourne la valeur de la clé. +// Si une fonction de découpage est fournie, la clé est recherchée de façon récursive après découpage. +func (o Json) Get(key string, split ...SplitFunc) (value Option[any]) { + if len(split) == 0 { + return o.get(key) + } + if keys, ok := SplitKey(key, split[0]).Ok(); ok { + return o.get(keys...) + } + + return +} + +// GetRecursive retourne la valeur de la clé (de façon récursive). +func (o Json) GetRecursive(keys ...string) (value Option[any]) { + return o.get(keys...) +} + +// Parse injecte la valeur associée à la clé dans out, si possible, +// et retourne vrai si l’injection a été possible. +// out doit être un pointeur. +// Si split est fourni la clé est découpée. +func (o Json) Parse(out any, key string, split ...SplitFunc) (ok bool) { + var v any + if v, ok = o.Get(key, split...).Get(); ok { + ok = convert.Convert(out, v) + } + + return +} + +// ParseRecurive agit comme Parse, récursivement. +func (o Json) ParseRecursive(out any, keys ...string) (ok bool) { + var v any + if v, ok = o.GetRecursive(keys...).Get(); ok { + ok = convert.Convert(out, v) + } + + return +} + +// SafeParse agit comme Parse mais la valeur trouvée doit être compatible avec le type de out. +func (o Json) SafeParse(out any, key string, split ...SplitFunc) (ok bool) { + var v any + if v, ok = o.Get(key, split...).Get(); ok { + ok = convert.Convert(out, v, true) + } + + return +} + +// SafeParseRecursive agit comme SafeParse, récursivement. +func (o Json) SafeParseRecursive(out any, keys ...string) (ok bool) { + var v any + if v, ok = o.GetRecursive(keys...).Get(); ok { + ok = convert.Convert(out, v, true) + } + + return +} + +func (o Json) set(value any, keys ...string) (ok bool) { + l := len(keys) + switch l { + case 0: + case 1: + o[keys[0]], ok = value, true + default: + ok = setAll(o, value, keys) + } + return +} + +// Set insère la valeur dans l’objet et retourne vrai si la valeur a été insérée. +// Si la fonction de découpage est fournie, la clé est découpée. +func (o Json) Set(value any, key string, split ...SplitFunc) (ok bool) { + if len(split) == 0 { + return o.set(value, key) + } + var keys []string + if keys, ok = SplitKey(key, split[0]).Ok(); ok { + return o.set(value, keys...) + } + return +} + +// SetRecursive insère la valeur de façon récursive dans l’objet et retourne vrai si elle existe. +func (o Json) SetRecursive(value any, keys ...string) (ok bool) { + return o.set(value, keys...) +} + +func (o Json) del(keys ...string) (ok bool) { + l := len(keys) + switch l { + case 0: + case 1: + if ok = o.Get(keys[0]).IsDefined(); ok { + delete(o, keys[0]) + } + default: + ok = delAll(o, keys) + } + return +} + +// Si la fonction de découpage est fournie, la clé est découpée. +func (o Json) Del(key string, split ...SplitFunc) (ok bool) { + if len(split) == 0 { + return o.del(key) + } + var keys []string + if keys, ok = SplitKey(key, split[0]).Ok(); ok { + return o.del(keys...) + } + return +} + +// DelRecursive supprimer la clé de façon récursive dans l’objet et retourne vrai si elle existe. +func (o Json) DelRecursive(keys ...string) (ok bool) { + return o.del(keys...) +} + +// Clone effectue une copie profonde de l’objet. +func (o Json) Clone(deep ...bool) Json { + c := make(Json) + for k, v := range o { + c.Set(convert.Clone(v, deep...), k) + } + return c +} + +// Fusion ajoute toutes les entrées de o2 dans o1 et retourne o1 +func (o1 Json) Fusion(o2 Json) Json { + for k, v := range o2 { + o1[k] = v + } + return o1 +} + +// Flat aplatit la structure du Json. +func (o Json) Flat(merge MergeFunc) (out Json) { + keys, values := flat(convert.ValueOf(o)) + out = make(Json) + for i, k := range keys { + out[merge(k)] = values[i] + } + return +} + +// Unflat est l’opération inverse de Flat. +func (o Json) Unflat(split SplitFunc) (out Json) { + vo := unflat(convert.ValueOf(o), split) + out = vo.Interface().(Json) + + for k, v := range out { + out[k] = slicify(convert.ValueOf(v)) + } + return +} diff --git a/collection/json/json_slice.go b/collection/json/json_slice.go new file mode 100644 index 0000000..17bb34a --- /dev/null +++ b/collection/json/json_slice.go @@ -0,0 +1,14 @@ +package json + +// Slice est un tableau d’objets JSON. +type Slice []Json + +func (sl *Slice) Add(objects ...Json) Slice { + *sl = append(*sl, objects...) + return *sl +} + +func (sl *Slice) Insert(objects ...Json) Slice { + *sl = append(objects, (*sl)...) + return *sl +}