Skip to content

Commit

Permalink
add JSON serialisation to Circle
Browse files Browse the repository at this point in the history
  • Loading branch information
mikenye committed Feb 10, 2025
1 parent 9fc36ac commit 3ad509f
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 33 deletions.
40 changes: 37 additions & 3 deletions circle/circle.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
package circle

import (
"encoding/json"
"fmt"
"github.com/mikenye/geom2d/numeric"
"github.com/mikenye/geom2d/options"
Expand Down Expand Up @@ -55,7 +56,7 @@ type Circle[T types.SignedNumber] struct {
func New[T types.SignedNumber](x, y, radius T) Circle[T] {
return Circle[T]{
center: point.New[T](x, y),
radius: radius,
radius: numeric.Abs(radius),
}
}

Expand All @@ -70,7 +71,7 @@ func New[T types.SignedNumber](x, y, radius T) Circle[T] {
func NewFromPoint[T types.SignedNumber](center point.Point[T], radius T) Circle[T] {
return Circle[T]{
center: center,
radius: radius,
radius: numeric.Abs(radius),
}
}

Expand Down Expand Up @@ -285,6 +286,17 @@ func (c Circle[T]) Eq(other Circle[T], opts ...options.GeometryOptionsFunc) bool
return centersEqual && radiiEqual
}

// MarshalJSON serializes Circle as JSON while preserving its original type.
func (c Circle[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Center point.Point[T] `json:"center"`
Radius T `json:"radius"`
}{
Center: c.center,
Radius: c.radius,
})
}

// Radius returns the radius of the Circle.
//
// Returns:
Expand Down Expand Up @@ -335,8 +347,10 @@ func (c Circle[T]) Rotate(pivot point.Point[T], radians float64, opts ...options
//
// Returns:
// - Circle[T]: A new circle with the radius scaled by the specified factor.
//
// todo: update doc comment, examples after adding numeric.Abs to radius
func (c Circle[T]) Scale(factor T) Circle[T] {
return Circle[T]{center: c.center, radius: c.radius * factor}
return Circle[T]{center: c.center, radius: numeric.Abs(c.radius * factor)}
}

// String returns a string representation of the Circle, including its center coordinates and radius.
Expand Down Expand Up @@ -365,6 +379,26 @@ func (c Circle[T]) Translate(v point.Point[T]) Circle[T] {
return Circle[T]{center: c.center.Translate(v), radius: c.radius}
}

// UnmarshalJSON deserializes JSON into a Circle while keeping the exact original type.
func (c *Circle[T]) UnmarshalJSON(data []byte) error {
var temp struct {
Center point.Point[T] `json:"center"`
Radius T `json:"radius"`
}
if err := json.Unmarshal(data, &temp); err != nil {
return err
}

Check warning on line 390 in circle/circle.go

View check run for this annotation

Codecov / codecov/patch

circle/circle.go#L389-L390

Added lines #L389 - L390 were not covered by tests

// Validate radius (ensure it's non-negative)
if temp.Radius < 0 {
return fmt.Errorf("invalid radius: must be non-negative, got %v", temp.Radius)
}

Check warning on line 395 in circle/circle.go

View check run for this annotation

Codecov / codecov/patch

circle/circle.go#L394-L395

Added lines #L394 - L395 were not covered by tests

c.center = temp.Center
c.radius = temp.Radius
return nil
}

// reflectAcrossCircleOctants generates a slice of points that represent the reflection
// of a given point (x, y) across all eight octants of a circle centered at (xc, yc).
//
Expand Down
70 changes: 67 additions & 3 deletions circle/circle_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package circle

import (
"encoding/json"
"github.com/mikenye/geom2d/options"
"github.com/mikenye/geom2d/point"
"github.com/mikenye/geom2d/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"math"
"testing"
)
Expand Down Expand Up @@ -316,6 +318,68 @@ func TestCircle_Eq(t *testing.T) {
}
}

func TestCircle_MarshalUnmarshalJSON(t *testing.T) {
tests := map[string]struct {
circle any // Input circle
expected any // Expected output after Marshal -> Unmarshal
}{
"Circle[int]": {
circle: NewFromPoint[int](point.New[int](3, 4), 5),
expected: NewFromPoint[int](point.New[int](3, 4), 5),
},
"Circle[int64]": {
circle: NewFromPoint[int64](point.New[int64](10, 20), 100),
expected: NewFromPoint[int64](point.New[int64](10, 20), 100),
},
"Circle[float32]": {
circle: NewFromPoint[float32](point.New[float32](1.5, 2.5), 4.5),
expected: NewFromPoint[float32](point.New[float32](1.5, 2.5), 4.5),
},
"Circle[float64]": {
circle: NewFromPoint[float64](point.New[float64](3.5, 7.2), 2.8),
expected: NewFromPoint[float64](point.New[float64](3.5, 7.2), 2.8),
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// Marshal
data, err := json.Marshal(tc.circle)
require.NoErrorf(t, err, "Failed to marshal %s: %v", tc.circle, err)

// Determine the correct type for unmarshalling
switch expected := tc.expected.(type) {
case Circle[int]:
var result Circle[int]
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

case Circle[int64]:
var result Circle[int64]
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

case Circle[float32]:
var result Circle[float32]
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

case Circle[float64]:
var result Circle[float64]
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

default:
t.Fatalf("Unhandled type in test case: %s", name)
}
})
}
}

func TestCircle_Radius(t *testing.T) {
tests := map[string]struct {
circle Circle[float64]
Expand All @@ -335,7 +399,7 @@ func TestCircle_Radius(t *testing.T) {
},
"negative radius (edge case)": {
circle: New[float64](3, 4, -5),
expected: -5,
expected: 5,
},
}

Expand Down Expand Up @@ -461,7 +525,7 @@ func TestCircle_Scale(t *testing.T) {
"scale with negative factor": {
circle: New[float64](3, 4, 5),
factor: -2,
expected: New[float64](3, 4, -10),
expected: New[float64](3, 4, 10),
},
}

Expand Down Expand Up @@ -489,7 +553,7 @@ func TestCircle_String(t *testing.T) {
},
"negative center and radius": {
circle: New[float64](-3.5, -4.5, -5.5),
expected: "(-3.5,-4.5; r=-5.5)",
expected: "(-3.5,-4.5; r=5.5)",
},
}

Expand Down
41 changes: 14 additions & 27 deletions rectangle/rectangle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/mikenye/geom2d/point"
"github.com/mikenye/geom2d/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"image"
"testing"
)
Expand Down Expand Up @@ -330,47 +331,33 @@ func TestRectangle_MarshalUnmarshalJSON(t *testing.T) {
t.Run(name, func(t *testing.T) {
// Marshal
data, err := json.Marshal(tc.rectangle)
if err != nil {
t.Fatalf("Failed to marshal %s: %v", name, err)
}
require.NoErrorf(t, err, "Failed to marshal %s: %v", tc.rectangle, err)

// Determine the correct type for unmarshalling
switch expected := tc.expected.(type) {
case Rectangle[int]:
var result Rectangle[int]
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal %s: %v", name, err)
}
if result != expected {
t.Errorf("%s: Expected %v, got %v", name, expected, result)
}
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

case Rectangle[int64]:
var result Rectangle[int64]
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal %s: %v", name, err)
}
if result != expected {
t.Errorf("%s: Expected %v, got %v", name, expected, result)
}
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

case Rectangle[float32]:
var result Rectangle[float32]
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal %s: %v", name, err)
}
if result != expected {
t.Errorf("%s: Expected %v, got %v", name, expected, result)
}
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

case Rectangle[float64]:
var result Rectangle[float64]
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal %s: %v", name, err)
}
if result != expected {
t.Errorf("%s: Expected %v, got %v", name, expected, result)
}
err := json.Unmarshal(data, &result)
require.NoErrorf(t, err, "Failed to unmarshal %s: %v", string(data), err)
assert.Equalf(t, expected, result, "Expected %v, got %v", expected, result)

default:
t.Fatalf("Unhandled type in test case: %s", name)
Expand Down

0 comments on commit 3ad509f

Please sign in to comment.