transform/typeconv/typeconv.go (181 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package typeconv import ( "errors" "fmt" "sync" "time" structform "github.com/elastic/go-structform" "github.com/elastic/go-structform/gotype" ) // Converter converts structured data between arbitrary typed (serializable) // go structures and maps/slices/arrays. It uses go-structform/gotype for input // and output values each, such that any arbitrary structures can be used. // // The converter computes and caches mapping operations for go structures it // has visited. type Converter struct { fold *gotype.Iterator unfold *gotype.Unfolder } type timeUnfolder struct { gotype.BaseUnfoldState to *time.Time a, b uint64 st timeUnfoldState } type timeUnfoldState uint8 const ( timeUnfoldInit timeUnfoldState = iota timeUnfoldDone timeUnfoldWaitA timeUnfoldWaitB timeUnfoldWaitDone ) var convPool = sync.Pool{ New: func() interface{} { return &Converter{} }, } // NewConverter creates a new converter with local state for tracking known // type conversations. func NewConverter() *Converter { c := &Converter{} return c } func (c *Converter) init() { unfold, _ := gotype.NewUnfolder(nil, gotype.Unfolders( unfoldTimestamp, )) fold, err := gotype.NewIterator(unfold, gotype.Folders( foldTimestamp, )) if err != nil { panic(err) } c.unfold = unfold c.fold = fold } // Convert transforms the value of from into to, by translating the structure // from into a set of events (go-structform.Visitor) that can applied to the // value given by to. // The operation fails if the values are not compatible (for example trying to // convert an object into an int), or `to` is no pointer. func (c *Converter) Convert(to, from interface{}) (err error) { if c.unfold == nil || c.fold == nil { c.init() } defer func() { if err != nil { c.fold = nil c.unfold = nil } }() if err = c.unfold.SetTarget(to); err != nil { return err } defer c.unfold.Reset() return c.fold.Fold(from) } // Convert transforms the value of from into to, by translating the structure // from into a set of events (go-structform.Visitor) that can applied to the // value given by to. // The operation fails if the values are not compatible (for example trying to // convert an object into an int). // To `to` parameter must be a pointer, otherwise the operation fails. // // Go structures can influence the transformation via tags using the `struct` namespace. // If the tag is missing, the structs field names are used. Additional options are separates by `,`. // options: // `squash`, `inline`: The fields in the child struct/map are assumed to be inlined, without reporting a sub-oject. // `omitempty`: The field is not converted if it is "empty". For example an // empty string, array or `nil` pointers are assumed to be empty. In either case the original value // in `to` will not be overwritten. // `omit`, `-`: Do not convert the field. func Convert(to, from interface{}) (err error) { c, _ := convPool.Get().(*Converter) defer convPool.Put(c) return c.Convert(to, from) } func foldTimestamp(in *time.Time, v structform.ExtVisitor) error { extra, sec := timestampToBits(*in) if err := v.OnArrayStart(2, structform.Uint64Type); err != nil { return err } if err := v.OnUint64(extra); err != nil { return err } if err := v.OnUint64(sec); err != nil { return err } if err := v.OnArrayFinished(); err != nil { return err } return nil } func unfoldTimestamp(to *time.Time) gotype.UnfoldState { return &timeUnfolder{to: to} } func (u *timeUnfolder) OnString(ctx gotype.UnfoldCtx, in string) (err error) { if u.st != timeUnfoldInit { return fmt.Errorf("unexpected string '%v' when trying to unfold a timestamp", in) } *u.to, err = time.Parse(time.RFC3339, in) u.st = timeUnfoldDone ctx.Done() return err } func (u *timeUnfolder) OnArrayStart(ctx gotype.UnfoldCtx, len int, _ structform.BaseType) error { if u.st != timeUnfoldInit { return errors.New("unexpected array") } if len >= 0 && len != 2 { return fmt.Errorf("%v is no valid encoded timestamp length", len) } u.st = timeUnfoldWaitA return nil } func (u *timeUnfolder) OnInt(ctx gotype.UnfoldCtx, in int64) error { return u.OnUint(ctx, uint64(in)) } func (u *timeUnfolder) OnFloat(ctx gotype.UnfoldCtx, f float64) error { return u.OnUint(ctx, uint64(f)) } func (u *timeUnfolder) OnUint(ctx gotype.UnfoldCtx, in uint64) error { switch u.st { case timeUnfoldWaitA: u.a = in u.st = timeUnfoldWaitB case timeUnfoldWaitB: u.b = in u.st = timeUnfoldWaitDone default: return fmt.Errorf("unexpected number '%v' in timestamp array", in) } return nil } func (u *timeUnfolder) OnArrayFinished(ctx gotype.UnfoldCtx) error { defer ctx.Done() if u.st != timeUnfoldWaitDone { return errors.New("unexpected timestamp array closed") } u.st = timeUnfoldDone ts, err := bitsToTimestamp(u.a, u.b) if err != nil { return err } *u.to = ts return nil } func timestampToBits(ts time.Time) (uint64, uint64) { var ( off int16 loc = ts.Location() ) const encodingVersion = 0 if loc == time.UTC { off = -1 } else { _, offset := ts.Zone() offset /= 60 // Note: best effort. If the zone offset has a factional minute, then we will ignore it here if offset < -32768 || offset == -1 || offset > 32767 { offset = 0 // Note: best effort. Ignore offset if it becomes an unexpected value } off = int16(offset) } sec := uint64(ts.Unix()) extra := (uint64(encodingVersion) << 56) | (uint64(uint16(off)) << 32) | uint64(ts.Nanosecond()) return extra, sec } func bitsToTimestamp(extra, sec uint64) (time.Time, error) { var ts time.Time version := (extra >> 56) & 0xff if version != 0 { return ts, fmt.Errorf("invalid timestamp [%x, %x]", extra, sec) } nsec := uint32(extra) off := int16(extra >> 32) ts = time.Unix(int64(sec), int64(nsec)) // adjust location by offset. time.Unix creates a timestamp in the local zone // by default. Only change this if off does not match the local zone it's offset. if off == -1 { ts = ts.UTC() } else if off != 0 { _, locOff := ts.Zone() if off != int16(locOff/60) { ts = ts.In(time.FixedZone("", int(off*60))) } } return ts, nil }