Repository: Masterminds/semver
Pull request #190 (2022-11-28 20:52:01)

Adding text marshaling and unmarshaling
Note, this removes the JSON specific handling as the general handling covers the JSON case.

If this is used with JSON and > or < are used than a custom encoder is required. In Go json.Marshal will escape those. For example...

    cs, _ := NewConstraint(tc.constraint)
    buf := new(bytes.Buffer)
    enc := json.NewEncoder(buf)
    enc.SetEscapeHTML(false)
    err = enc.Encode(cs)
    jsonVal = buf.String()



Patch:
diff --git a/constraints.go b/constraints.go
index 492364b..203072e 100644
--- a/constraints.go
+++ b/constraints.go
@@ -2,7 +2,6 @@ package semver
 
 import (
 	"bytes"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"regexp"
@@ -135,31 +134,21 @@ func (cs Constraints) String() string {
 	return strings.Join(buf, " || ")
 }
 
-// UnmarshalJSON implements JSON.Unmarshaler interface.
-func (cs *Constraints) UnmarshalJSON(b []byte) error {
-	var s string
-	if err := json.Unmarshal(b, &s); err != nil {
-		return err
-	}
-	temp, err := NewConstraint(s)
+// UnmarshalText implements the encoding.TextUnmarshaler interface.
+func (cs *Constraints) UnmarshalText(text []byte) error {
+	temp, err := NewConstraint(string(text))
 	if err != nil {
 		return err
 	}
+
 	*cs = *temp
+
 	return nil
 }
 
-// MarshalJSON implements JSON.Marshaler interface.
-func (cs Constraints) MarshalJSON() ([]byte, error) {
-	// we need our own encoder so we don't escape '<' and '>' which json.Marshal does
-	buf := new(bytes.Buffer)
-	enc := json.NewEncoder(buf)
-	enc.SetEscapeHTML(false)
-
-	if err := enc.Encode(cs.String()); err != nil {
-		return nil, err
-	}
-	return bytes.TrimRight(buf.Bytes(), "\n"), nil
+// MarshalText implements the encoding.TextMarshaler interface.
+func (cs Constraints) MarshalText() ([]byte, error) {
+	return []byte(cs.String()), nil
 }
 
 var constraintOps map[string]cfunc


Test patch:
diff --git a/constraints_test.go b/constraints_test.go
index 803877f..5efa92d 100644
--- a/constraints_test.go
+++ b/constraints_test.go
@@ -680,55 +680,91 @@ func TestConstraintString(t *testing.T) {
 	}
 }
 
-func TestJsonMarshalConstraints(t *testing.T) {
+func TestTextMarshalConstraints(t *testing.T) {
 	tests := []struct {
-		sCs  string
-		want string
+		constraint string
+		want       string
 	}{
-		{"1.1.1", "1.1.1"},
-		{">=1.1.1", ">=1.1.1"},
-		{"<=1.1.1", "<=1.1.1"},
+		{"1.2.3", "1.2.3"},
+		{">=1.2.3", ">=1.2.3"},
+		{"<=1.2.3", "<=1.2.3"},
+		{"1 <=1.2.3", "1 <=1.2.3"},
+		{"1, <=1.2.3", "1 <=1.2.3"},
+		{">1, <=1.2.3", ">1 <=1.2.3"},
+		{"> 1 , <=1.2.3", ">1 <=1.2.3"},
 	}
 
 	for _, tc := range tests {
-		cs, err := NewConstraint(tc.sCs)
+		cs, err := NewConstraint(tc.constraint)
 		if err != nil {
 			t.Errorf("Error creating constraints: %s", err)
 		}
+
+		out, err2 := cs.MarshalText()
+		if err2 != nil {
+			t.Errorf("Error constraint version: %s", err2)
+		}
+
+		got := string(out)
+		if got != tc.want {
+			t.Errorf("Error marshaling constraint, unexpected marshaled content: got=%q want=%q", got, tc.want)
+		}
+
+		// Test that this works for JSON as well as text. When JSON marshaling
+		// functions are missing it falls through to TextMarshal.
+		// NOTE: To not escape the < and > (which json.Marshal does) you need
+		// a custom encoder where html escaping is disabled. This must be done
+		// in the top level encoder being used to marshal the constraints.
 		buf := new(bytes.Buffer)
 		enc := json.NewEncoder(buf)
 		enc.SetEscapeHTML(false)
 		err = enc.Encode(cs)
 		if err != nil {
-			t.Errorf("Error unmarshaling version: %s", err)
+			t.Errorf("Error unmarshaling constraint: %s", err)
 		}
-		got := buf.String()
+		got = buf.String()
+		// The encoder used here adds a newline so we add that to what we want
+		// so they align. The newline is an artifact of the testing.
 		want := fmt.Sprintf("%q\n", tc.want)
 		if got != want {
-			t.Errorf("Error marshaling unexpected marshaled content: got=%q want=%q", got, want)
+			t.Errorf("Error marshaling constraint, unexpected marshaled content: got=%q want=%q", got, want)
 		}
 	}
 }
 
-func TestJsonUnmarshalConstraints(t *testing.T) {
+func TestTextUnmarshalConstraints(t *testing.T) {
 	tests := []struct {
-		sCs  string
-		want string
+		constraint string
+		want       string
 	}{
-		{"1.1.1", "1.1.1"},
+		{"1.2.3", "1.2.3"},
 		{">=1.2.3", ">=1.2.3"},
 		{"<=1.2.3", "<=1.2.3"},
+		{">1 <=1.2.3", ">1 <=1.2.3"},
+		{"> 1 <=1.2.3", ">1 <=1.2.3"},
+		{">1, <=1.2.3", ">1 <=1.2.3"},
 	}
 
 	for _, tc := range tests {
 		cs := Constraints{}
-		err := json.Unmarshal([]byte(fmt.Sprintf("%q", tc.sCs)), &cs)
+		err := cs.UnmarshalText([]byte(tc.constraint))
 		if err != nil {
 			t.Errorf("Error unmarshaling constraints: %s", err)
 		}
 		got := cs.String()
 		if got != tc.want {
-			t.Errorf("Error unmarshaling unexpected object content: got=%q want=%q", got, tc.want)
+			t.Errorf("Error unmarshaling constraint, unexpected object content: got=%q want=%q", got, tc.want)
+		}
+
+		// Test that this works for JSON as well as text. When JSON unmarshaling
+		// functions are missing it falls through to TextUnmarshal.
+		err = json.Unmarshal([]byte(fmt.Sprintf("%q", tc.constraint)), &cs)
+		if err != nil {
+			t.Errorf("Error unmarshaling constraints: %s", err)
+		}
+		got = cs.String()
+		if got != tc.want {
+			t.Errorf("Error unmarshaling constraint, unexpected object content: got=%q want=%q", got, tc.want)
 		}
 	}
 }