Skip to content

Instantly share code, notes, and snippets.

@squeedee
Created February 7, 2025 03:07
Show Gist options
  • Save squeedee/b182a819262443312d047dac5275ad34 to your computer and use it in GitHub Desktop.
Save squeedee/b182a819262443312d047dac5275ad34 to your computer and use it in GitHub Desktop.
intrinsic scalar typed json/yaml in golang
package api
import (
"encoding/json"
"fmt"
)
// Value can store either a string or float64 value
type Value struct {
stringValue *string
intVal *int
floatVal *float64
}
func NewIntValue(val int) *Value {
return &Value{
intVal: &val,
}
}
func NewFloatValue(val float64) *Value {
return &Value{
floatVal: &val,
}
}
func NewStringValue(val string) *Value {
return &Value{
stringValue: &val,
}
}
func NewNilValue() *Value {
return &Value{}
}
// IsNil returns true if the value is null
func (d *Value) IsNil() bool {
return d.stringValue == nil && d.intVal == nil && d.floatVal == nil
}
// MarshalJSON implements yaml.Marshaler
func (d *Value) MarshalJSON() ([]byte, error) {
if d.stringValue != nil {
return json.Marshal(d.stringValue)
}
if d.intVal != nil {
return json.Marshal(*d.intVal)
}
if d.floatVal != nil {
return json.Marshal(*d.floatVal)
}
return json.Marshal(nil)
}
// UnmarshalJSON implements yaml.Unmarshaler
func (d *Value) UnmarshalJSON(data []byte) error {
// Handle null value
if string(data) == "null" {
return nil
}
// Try as number first
var f float64
if err := json.Unmarshal(data, &f); err == nil {
// Check if it's an integer by looking for decimal point
s := string(data)
if s[0] != '"' && len(s) > 0 { // ensure it's not a string
hasDecimal := false
for _, c := range s {
if c == '.' {
hasDecimal = true
break
}
}
if !hasDecimal {
i := int(f)
d.intVal = &i
return nil
}
}
d.floatVal = &f
return nil
}
s := ""
d.stringValue = &s
// Try as string
if err := json.Unmarshal(data, d.stringValue); err == nil {
return nil
}
return fmt.Errorf("value must be either a number or string")
}
func (d *Value) Type() string {
if d.stringValue != nil {
return "string"
}
if d.intVal != nil {
return "int"
}
if d.floatVal != nil {
return "float"
}
return "nil"
}
package api_test
import (
"encoding/json"
"github.com/squeedee/ideclare/api"
"github.com/stretchr/testify/assert"
"testing"
)
func TestMarshalUnmarshal(t *testing.T) {
tests := map[string]struct {
json string
expectNull bool
}{
"float": {
json: "3.14",
},
"int": {
json: "3",
},
"string": {
json: "\"hi friend\"",
},
"nil": {
json: "null",
expectNull: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
v := &api.Value{}
err := json.Unmarshal([]byte(tc.json), v)
assert.NoError(t, err)
r, err := json.Marshal(v)
assert.NoError(t, err)
assert.Equal(t, tc.json, string(r))
assert.Equal(t, name, v.Type())
assert.Equal(t, tc.expectNull, v.IsNil())
})
}
}
func TestConstructors(t *testing.T) {
tests := map[string]struct {
constructor func() *api.Value
expectValue string
expectNull bool
}{
"float": {
constructor: func() *api.Value {
return api.NewFloatValue(3.14)
},
expectValue: "3.14",
},
"int": {
constructor: func() *api.Value {
return api.NewIntValue(3)
},
expectValue: "3",
},
"string": {
constructor: func() *api.Value {
return api.NewStringValue("hi friend")
},
expectValue: "\"hi friend\"",
},
"nil": {
constructor: func() *api.Value {
return api.NewNilValue()
},
expectValue: "null",
expectNull: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
v := tc.constructor()
r, err := json.Marshal(v)
assert.NoError(t, err)
assert.Equal(t, tc.expectValue, string(r))
assert.Equal(t, name, v.Type())
assert.Equal(t, tc.expectNull, v.IsNil())
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment