// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. // All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package schema import ( "errors" "io" "math" "strconv" "github.com/ClusterCockpit/cc-backend/pkg/log" ) // A custom float type is used so that (Un)MarshalJSON and // (Un)MarshalGQL can be overloaded and NaN/null can be used. // The default behaviour of putting every nullable value behind // a pointer has a bigger overhead. type Float float64 var NaN Float = Float(math.NaN()) var nullAsBytes []byte = []byte("null") func (f Float) IsNaN() bool { return math.IsNaN(float64(f)) } // NaN will be serialized to `null`. func (f Float) MarshalJSON() ([]byte, error) { if f.IsNaN() { return nullAsBytes, nil } return strconv.AppendFloat(make([]byte, 0, 10), float64(f), 'f', 2, 64), nil } // `null` will be unserialized to NaN. func (f *Float) UnmarshalJSON(input []byte) error { s := string(input) if s == "null" { *f = NaN return nil } val, err := strconv.ParseFloat(s, 64) if err != nil { log.Warn("Error while parsing custom float") return err } *f = Float(val) return nil } // UnmarshalGQL implements the graphql.Unmarshaler interface. func (f *Float) UnmarshalGQL(v interface{}) error { f64, ok := v.(float64) if !ok { return errors.New("invalid Float scalar") } *f = Float(f64) return nil } // MarshalGQL implements the graphql.Marshaler interface. // NaN will be serialized to `null`. func (f Float) MarshalGQL(w io.Writer) { if f.IsNaN() { w.Write(nullAsBytes) } else { w.Write(strconv.AppendFloat(make([]byte, 0, 10), float64(f), 'f', 2, 64)) } } // Only used via REST-API, not via GraphQL. // This uses a lot less allocations per series, // but it turns out that the performance increase // from using this is not that big. func (s *Series) MarshalJSON() ([]byte, error) { buf := make([]byte, 0, 512+len(s.Data)*8) buf = append(buf, `{"hostname":"`...) buf = append(buf, s.Hostname...) buf = append(buf, '"') if s.Id != nil { buf = append(buf, `,"id":`...) buf = strconv.AppendInt(buf, int64(*s.Id), 10) } if s.Statistics != nil { buf = append(buf, `,"statistics":{"min":`...) buf = strconv.AppendFloat(buf, s.Statistics.Min, 'f', 2, 64) buf = append(buf, `,"avg":`...) buf = strconv.AppendFloat(buf, s.Statistics.Avg, 'f', 2, 64) buf = append(buf, `,"max":`...) buf = strconv.AppendFloat(buf, s.Statistics.Max, 'f', 2, 64) buf = append(buf, '}') } buf = append(buf, `,"data":[`...) for i := 0; i < len(s.Data); i++ { if i != 0 { buf = append(buf, ',') } if s.Data[i].IsNaN() { buf = append(buf, `null`...) } else { buf = strconv.AppendFloat(buf, float64(s.Data[i]), 'f', 2, 32) } } buf = append(buf, ']', '}') return buf, nil }