3 Commits
Author SHA1 Message Date
esilva 2acdb8fb3a Follow the fucking spec. A '.' ain't no ',' 2025-09-25 17:40:36 +01:00
esilva ab55a748ab Marshalling 2025-09-14 15:44:07 +01:00
esilva b2bca295e0 Added benchmark test 2025-09-08 19:09:04 +01:00
5 changed files with 217 additions and 76 deletions
+12
View File
@@ -0,0 +1,12 @@
version: "2"
linters:
default: all
disable:
- wsl
- depguard
- varnamelen
- wrapcheck
- wsl_v5
- exhaustruct
- gochecknoglobals
- nlreturn
+3
View File
@@ -2,3 +2,6 @@ test:
go test -coverprofile=coverage.out ./... go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html go tool cover -html=coverage.out -o coverage.html
rm coverage.out rm coverage.out
bench:
go test -bench=. -cpuprofile=cpu.out -memprofile=mem.out
+18 -2
View File
@@ -5,8 +5,8 @@ import (
"testing" "testing"
) )
func TestFatQRAPI(t *testing.T) { var (
input := "A:123456789*B:999999990*C:PT*D:FT*E:N*F:20191231*" + completeInput = "A:123456789*B:999999990*C:PT*D:FT*E:N*F:20191231*" +
"G:FT AB2019/0035*H:CSDF7T5H-0035*I1:PT*I2:12000.00*I3:15000.00*" + "G:FT AB2019/0035*H:CSDF7T5H-0035*I1:PT*I2:12000.00*I3:15000.00*" +
"I4:900.00*I5:50000.00*I6:6500.00*I7:80000.00*I8:18400.00*J1:PT-AC*" + "I4:900.00*I5:50000.00*I6:6500.00*I7:80000.00*I8:18400.00*J1:PT-AC*" +
"J2:10000.00*J3:25000.56*J4:1000.02*J5:75000.00*J6:6750.00*" + "J2:10000.00*J3:25000.56*J4:1000.02*J5:75000.00*J6:6750.00*" +
@@ -14,6 +14,10 @@ func TestFatQRAPI(t *testing.T) {
"K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" + "K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" +
"N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" + "N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" +
"S:TB;PT00000000000000000000000;513500.58" "S:TB;PT00000000000000000000000;513500.58"
)
func TestFatQRAPI(t *testing.T) {
input := completeInput
var qr gofatqr.FatQR var qr gofatqr.FatQR
err := qr.Scan(input, 0) err := qr.Scan(input, 0)
@@ -21,3 +25,15 @@ func TestFatQRAPI(t *testing.T) {
t.Errorf("Should not have occurred an error for: %s, %+v", input, err) t.Errorf("Should not have occurred an error for: %s, %+v", input, err)
} }
} }
func BenchmarkFatQRScan(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var qr gofatqr.FatQR
err := qr.Scan(completeInput, 0)
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}
+93 -69
View File
@@ -1,3 +1,7 @@
// Package gofatqr offers a way to parse the QR code string from a portuguese
// invoice. It's up to date with the tech specs
// 'Portaria nº 195/2025 Versão 1.1 Outubro 2020'
// https://info.portaldasfinancas.gov.pt/pt/apoio_contribuinte/Novas_regras_faturacao/Documents/Especificacoes_Tecnicas_Codigo_QR.pdf
package gofatqr package gofatqr
// TODO // TODO
@@ -5,6 +9,7 @@ package gofatqr
// implementation too, who knows amirite // implementation too, who knows amirite
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -13,48 +18,57 @@ import (
dec "github.com/shopspring/decimal" dec "github.com/shopspring/decimal"
) )
// ScanMode defines the type used to configure stricter scanning and parsing of
// the code.
type ScanMode uint32 type ScanMode uint32
// Setting this up for the future // Setting this up for the future.
const ( const (
Strict ScanMode = 1 << iota Strict ScanMode = 1 << iota
NifValidation NifValidation
) )
const (
decCount = 2
separator = "*"
)
// FiscalSpace Represents the each of the fiscal zones the invoice can have.
type FiscalSpace struct { type FiscalSpace struct {
TaxCountryRegion string // 1 - fiscal space TaxCountryRegion string `json:"TaxCountryRegion"` // 1
TaxableBase *dec.Decimal // 2 TaxableBase *dec.Decimal `json:"TaxableBase"` // 2
TaxableReduced *dec.Decimal // 3 TaxableReduced *dec.Decimal `json:"TaxableReduced"` // 3
VatTotalReducedTax *dec.Decimal // 4 VatTotalReducedTax *dec.Decimal `json:"VatTotalReducedTax"` // 4
TaxableIntermediateBase *dec.Decimal // 5 TaxableIntermediateBase *dec.Decimal `json:"TaxableIntermediateBase"` // 5
VatTotalIntermediateTax *dec.Decimal // 6 VatTotalIntermediateTax *dec.Decimal `json:"VatTotalIntermediateTax"` // 6
TaxableNormalBase *dec.Decimal // 7 TaxableNormalBase *dec.Decimal `json:"TaxableNormalBase"` // 7
VatTotalNormalBase *dec.Decimal // 8 VatTotalNormalBase *dec.Decimal `json:"VatTotalNormalBase"` // 8
} }
// FatQR represents the data from the QR code of an invoice.
type FatQR struct { type FatQR struct {
TaxRegistrationNumber string // A TaxRegistrationNumber string `json:"TaxRegistrationNumber"` // A
CustomerTaxID string // B CustomerTaxID string `json:"CustomerTaxID"` // B
Country string // C customer country ISO 3166-1 alpha2 Country string `json:"Country"` // C country ISO 3166-1 alpha2
InvoiceType string // D InvoiceType string `json:"InvoiceType"` // D
InvoiceStatus string // E InvoiceStatus string `json:"InvoiceStatus"` // E
InvoiceDate time.Time // F InvoiceDate time.Time `json:"InvoiceDate"` // F
InvoiceNo string // G InvoiceNo string `json:"InvoiceNo"` // G
ATCUD string // H ATCUD string `json:"ATCUD"` // H
IFiscalSpace FiscalSpace // I1-I8 IFiscalSpace FiscalSpace `json:"IFiscalSpace"` // I1-I8
JFiscalSpace FiscalSpace // I1-I8 JFiscalSpace FiscalSpace `json:"JFiscalSpace"` // I1-I8
KFiscalSpace FiscalSpace // I1-I8 KFiscalSpace FiscalSpace `json:"KFiscalSpace"` // I1-I8
NotTaxable *dec.Decimal // L NotTaxable *dec.Decimal `json:"NotTaxable"` // L
StampDuty *dec.Decimal // M "Imposto de Selo" StampDuty *dec.Decimal `json:"StampDuty"` // M "Imposto de Selo"
TaxPayable *dec.Decimal // N TaxPayable *dec.Decimal `json:"TaxPayable"` // N
GrossTotal *dec.Decimal // O GrossTotal *dec.Decimal `json:"GrossTotal"` // O
WithholdingTaxAmount *dec.Decimal // P WithholdingTaxAmount *dec.Decimal `json:"WithholdingTaxAmount"` // P
HashQuartet string // Q HashQuartet string `json:"HashQuartet"` // Q
SoftwareCertificateNumber int64 // R SWCertNo int64 `json:"SWCertNo"` // R
OtherInfo string // S OtherInfo string `json:"OtherInfo"` // S
} }
type FieldCodec struct { type fieldCodec struct {
Required bool Required bool
Parse func(f *FatQR, val string) error Parse func(f *FatQR, val string) error
String func(f *FatQR) string String func(f *FatQR) string
@@ -69,15 +83,9 @@ var (
"K1", "K2", "K3", "K4", "K5", "K6", "K7", "K8", "K1", "K2", "K3", "K4", "K5", "K6", "K7", "K8",
"L", "M", "N", "O", "P", "Q", "R", "S", "L", "M", "N", "O", "P", "Q", "R", "S",
} }
// lastKey = fieldOrder[len(fieldOrder)-1]
) )
const ( var fatQRFieldMap = map[string]fieldCodec{
decCount = 2
separator = "*"
)
var fatQRFieldMap = map[string]FieldCodec{
"A": { "A": {
Required: true, Required: true,
Parse: func(f *FatQR, val string) error { Parse: func(f *FatQR, val string) error {
@@ -191,8 +199,7 @@ var fatQRFieldMap = map[string]FieldCodec{
"H": { "H": {
Required: true, Required: true,
Parse: func(f *FatQR, val string) error { Parse: func(f *FatQR, val string) error {
// TODO // TODO - add some ATCUD validation
// Add some ATCUD validation
f.ATCUD = val f.ATCUD = val
return nil return nil
}, },
@@ -571,7 +578,7 @@ var fatQRFieldMap = map[string]FieldCodec{
Required: true, Required: true,
Parse: func(f *FatQR, val string) error { Parse: func(f *FatQR, val string) error {
var err error var err error
f.SoftwareCertificateNumber, err = strconv.ParseInt(val, 10, 16) f.SWCertNo, err = strconv.ParseInt(val, 10, 16)
if err != nil { if err != nil {
return err return err
} }
@@ -579,10 +586,10 @@ var fatQRFieldMap = map[string]FieldCodec{
return nil return nil
}, },
String: func(f *FatQR) string { String: func(f *FatQR) string {
return "R:" + fmt.Sprintf("%04d", f.SoftwareCertificateNumber) return "R:" + fmt.Sprintf("%04d", f.SWCertNo)
}, },
Empty: func(f *FatQR) bool { Empty: func(f *FatQR) bool {
return f.SoftwareCertificateNumber < 1 return f.SWCertNo < 1
}, },
}, },
"S": { "S": {
@@ -604,6 +611,10 @@ func parseDecimal(f **dec.Decimal, val string) error {
if *f == nil { if *f == nil {
*f = new(dec.Decimal) *f = new(dec.Decimal)
} }
// Because software #2426 doesn't follow the spec correctly
val = strings.ReplaceAll(val, ",", ".")
return (*f).Scan(val) return (*f).Scan(val)
} }
@@ -611,38 +622,21 @@ func stringDecimal(d *dec.Decimal) string {
return d.Truncate(decCount).StringFixed(decCount) return d.Truncate(decCount).StringFixed(decCount)
} }
func (fq *FatQR) scanPart(part []string) error { // ToJSON returns a []byte with the FatQR data as JSON.
val, ok := fatQRFieldMap[part[0]] func (fq *FatQR) ToJSON() []byte {
if !ok { jsondata, _ := json.Marshal(fq)
return fmt.Errorf("invalid key `%s`", part[0]) return jsondata
}
err := val.Parse(fq, part[1])
if err != nil {
return err
}
return nil
} }
func (fq *FatQR) scanParts(parts []string) error { // FromJSON scans the data from a JSON []byte.
for k := range parts { func (fq *FatQR) FromJSON(data []byte) error {
keyValuePair := strings.Split(parts[k], ":") return json.Unmarshal(data, fq)
if len(keyValuePair) != 2 {
return fmt.Errorf("failed to parse key value pair: %s", parts[k])
}
err := fq.scanPart(keyValuePair)
if err != nil {
return err
}
}
return nil
} }
// Scan scans a given string and applies the rules of `mode` (to be defined) // Scan scans a given string and applies the rules of `mode` (to be defined).
func (fq *FatQR) Scan(s string, mode ScanMode) error { //
// returns an error if it fails to scan the given string.
func (fq *FatQR) Scan(s string, _ ScanMode) error {
parts := strings.Split(s, "*") parts := strings.Split(s, "*")
err := fq.scanParts(parts) err := fq.scanParts(parts)
@@ -672,3 +666,33 @@ func (fq *FatQR) String() string {
return strings.Join(parts, separator) return strings.Join(parts, separator)
} }
func (fq *FatQR) scanPart(part []string) error {
val, ok := fatQRFieldMap[part[0]]
if !ok {
return fmt.Errorf("invalid key `%s`", part[0])
}
err := val.Parse(fq, part[1])
if err != nil {
return err
}
return nil
}
func (fq *FatQR) scanParts(parts []string) error {
for k := range parts {
keyValuePair := strings.SplitN(parts[k], ":", 2)
if len(keyValuePair) != 2 {
return fmt.Errorf("failed to parse key value pair: %s", parts[k])
}
err := fq.scanPart(keyValuePair)
if err != nil {
return err
}
}
return nil
}
+91 -5
View File
@@ -11,14 +11,17 @@ import (
func TestScan(t *testing.T) { func TestScan(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
input string input string
output string // set output empty if its supposed to be equal to input
// once again, thanks to the fucking guys at 2426
}{ }{
{ {
name: "5.0. Example 0 - Some invoice I had laying around", name: "5.0. Example 0 - Some invoice I had laying around",
input: "A:513847588*B:132772985*C:PT*D:FS*E:N*F:20250828*" + input: "A:513847588*B:132772985*C:PT*D:FS*E:N*F:20250828*" +
"G:FS 437251001/115666*H:0*I1:PT*I7:37.94*I8:8.73*" + "G:FS 437251001/115666*H:0*I1:PT*I7:37.94*I8:8.73*" +
"N:8.73*O:46.67*Q:mWUp*R:0177", "N:8.73*O:46.67*Q:mWUp*R:0177",
output: "",
}, },
{ {
name: "5.1. Example 1 - Invoice", name: "5.1. Example 1 - Invoice",
@@ -30,12 +33,14 @@ func TestScan(t *testing.T) {
"K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" + "K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" +
"N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" + "N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" +
"S:TB;PT00000000000000000000000;513500.58", "S:TB;PT00000000000000000000000;513500.58",
output: "",
}, },
{ {
name: "5.2. Example 2 - Simplified Invoice", name: "5.2. Example 2 - Simplified Invoice",
input: "A:123456789*B:999999990*C:PT*D:FS*E:N*F:20190812*" + input: "A:123456789*B:999999990*C:PT*D:FS*E:N*F:20190812*" +
"G:FS CDVF/12345*H:CDF7T5HD-12345*I1:PT*I7:0.65*I8:0.15*N:0.15*O:0.80*" + "G:FS CDVF/12345*H:CDF7T5HD-12345*I1:PT*I7:0.65*I8:0.15*N:0.15*O:0.80*" +
"Q:YhGV*R:9999*S:NU;0.80", "Q:YhGV*R:9999*S:NU;0.80",
output: "",
}, },
{ {
name: "5.3. Example 3 - Pro-form Invoice", name: "5.3. Example 3 - Pro-form Invoice",
@@ -43,29 +48,54 @@ func TestScan(t *testing.T) {
"G:PF G2019CB/145789*H:HB6FT7RV-145789*I1:PT*I2:12345.34*I3:12532.65*" + "G:PF G2019CB/145789*H:HB6FT7RV-145789*I1:PT*I2:12345.34*I3:12532.65*" +
"I4:751.96*I5:52789.00*I6:6862.57*I7:32425.69*I8:7457.91*N:15072.44*" + "I4:751.96*I5:52789.00*I6:6862.57*I7:32425.69*I8:7457.91*N:15072.44*" +
"O:125165.12*Q:r/fY*R:9999", "O:125165.12*Q:r/fY*R:9999",
output: "",
}, },
{ {
name: "5.4. Example 4 - Transport Document", name: "5.4. Example 4 - Transport Document",
input: "A:500000000*B:123456789*C:PT*D:GT*E:N*F:20190720*" + input: "A:500000000*B:123456789*C:PT*D:GT*E:N*F:20190720*" +
"G:GT G234CB/50987*H:GTVX4Y8B-50987*I1:0*N:0.00*O:0.00*Q:5uIg*R:9999", "G:GT G234CB/50987*H:GTVX4Y8B-50987*I1:0*N:0.00*O:0.00*Q:5uIg*R:9999",
output: "",
}, },
{ {
name: "5.5. Example 5 - Invoice with foreign tax", name: "5.5. Example 5 - Invoice with foreign tax",
input: "A:123456789*B:4443332215*C:FR*D:FT*E:N*F:20190526*" + input: "A:123456789*B:4443332215*C:FR*D:FT*E:N*F:20190526*" +
"G:ABC BNH/4561*H:DK5ZJ2HN-4561*I1:FR*I7:100.00*I8:20.00*N:20.00*" + "G:ABC BNH/4561*H:DK5ZJ2HN-4561*I1:FR*I7:100.00*I8:20.00*N:20.00*" +
"O:120.00*Q:YJRE*R:9999", "O:120.00*Q:YJRE*R:9999",
output: "",
}, },
{ {
name: "5.6. Example 6 - Tax Rectification Debit Note", name: "5.6. Example 6 - Tax Rectification Debit Note",
input: "A:123456789*B:500000000*C:PT*D:ND*E:N*F:20190216*" + input: "A:123456789*B:500000000*C:PT*D:ND*E:N*F:20190216*" +
"G:M1F KLG/6145*H: RQD8L6DG-6145*I1:PT-MA*I6:26.50*N:26.50*O:26.50*" + "G:M1F KLG/6145*H: RQD8L6DG-6145*I1:PT-MA*I6:26.50*N:26.50*O:26.50*" +
"Q:h1rB*R:9999", "Q:h1rB*R:9999",
output: "",
}, },
{ {
name: "5.7. Example 7 - Margin", name: "5.7. Example 7 - Margin",
input: "A:500000000*B:123456789*C:PT*D:FT*E:N*F:20191124*" + input: "A:500000000*B:123456789*C:PT*D:FT*E:N*F:20191124*" +
"G:NF 19A/789145*H:JL9DS4TT-789145*I1:PT-AC*I7:50.00*I8:9.00*L:1000.00*" + "G:NF 19A/789145*H:JL9DS4TT-789145*I1:PT-AC*I7:50.00*I8:9.00*L:1000.00*" +
"N:9.00*O:1059.00*Q:d8/K*R:9999", "N:9.00*O:1059.00*Q:d8/K*R:9999",
output: "",
},
{
// Literally goes against the spec and we should be expecting failure here
name: "Some dev doesn't know how to read a spec file so periods are commas now",
input: "A:123456789*B:999999990*C:PT*D:FT*E:N*F:20191231*" +
"G:FT AB2019/0035*H:CSDF7T5H-0035*I1:PT*I2:12000,00*I3:15000,00*" +
"I4:900.00*I5:50000.00*I6:6500.00*I7:80000.00*I8:18400.00*J1:PT-AC*" +
"J2:10000.00*J3:25000.56*J4:1000.02*J5:75000.00*J6:6750,00*" +
"J7:100000.00*J8:18000.00*K1:PT-MA*K2:5000,00*K3:12500,00*K4:625.00*" +
"K5:25000.00*K6:3000,00*K7:40000,00*K8:8800,00*L:100.00*M:25.00*" +
"N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" +
"S:TB;PT00000000000000000000000;513500.58",
output: "A:123456789*B:999999990*C:PT*D:FT*E:N*F:20191231*" +
"G:FT AB2019/0035*H:CSDF7T5H-0035*I1:PT*I2:12000.00*I3:15000.00*" +
"I4:900.00*I5:50000.00*I6:6500.00*I7:80000.00*I8:18400.00*J1:PT-AC*" +
"J2:10000.00*J3:25000.56*J4:1000.02*J5:75000.00*J6:6750.00*" +
"J7:100000.00*J8:18000.00*K1:PT-MA*K2:5000.00*K3:12500.00*K4:625.00*" +
"K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" +
"N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" +
"S:TB;PT00000000000000000000000;513500.58",
}, },
} }
@@ -77,8 +107,13 @@ func TestScan(t *testing.T) {
t.Errorf("Should not have occurred an error for: %s, %+v", tc.input, err) t.Errorf("Should not have occurred an error for: %s, %+v", tc.input, err)
} }
expect := tc.input
if tc.output != "" {
expect = tc.output
}
stringified := qr.String() stringified := qr.String()
if diff := cmp.Diff(tc.input, stringified); diff != "" { if diff := cmp.Diff(expect, stringified); diff != "" {
t.Errorf("String mismatch (-want +got):\n%s", diff) t.Errorf("String mismatch (-want +got):\n%s", diff)
} }
} }
@@ -285,5 +320,56 @@ func TestScanPartI2(t *testing.T) {
} }
} }
// TODO: func TestMarshalling(t *testing.T) {
// Add more UTs input := "A:123456789*B:999999990*C:PT*D:FT*E:N*F:20191231*" +
"G:FT AB2019/0035*H:CSDF7T5H-0035*I1:PT*I2:12000.00*I3:15000.00*" +
"I4:900.00*I5:50000.00*I6:6500.00*I7:80000.00*I8:18400.00*J1:PT-AC*" +
"J2:10000.00*J3:25000.56*J4:1000.02*J5:75000.00*J6:6750.00*" +
"J7:100000.00*J8:18000.00*K1:PT-MA*K2:5000.00*K3:12500.00*K4:625.00*" +
"K5:25000.00*K6:3000.00*K7:40000.00*K8:8800.00*L:100.00*M:25.00*" +
"N:64000.02*O:513600.58*P:100.00*Q:kLp0*R:9999*" +
"S:TB;PT00000000000000000000000;513500.58"
expected := "{\"TaxRegistrationNumber\":\"123456789\",\"CustomerTaxID\":\"999999990\"," +
"\"Country\":\"PT\",\"InvoiceType\":\"FT\",\"InvoiceStatus\":\"N\"," +
"\"InvoiceDate\":\"2019-12-31T00:00:00Z\",\"InvoiceNo\":\"FT AB2019/0035\"," +
"\"ATCUD\":\"CSDF7T5H-0035\",\"IFiscalSpace\":{\"TaxCountryRegion\":\"PT\"," +
"\"TaxableBase\":\"12000\",\"TaxableReduced\":\"15000\",\"VatTotalReducedTax\":\"900\"," +
"\"TaxableIntermediateBase\":\"50000\",\"VatTotalIntermediateTax\":\"6500\"," +
"\"TaxableNormalBase\":\"80000\",\"VatTotalNormalBase\":\"18400\"}," +
"\"JFiscalSpace\":{\"TaxCountryRegion\":\"PT-AC\",\"TaxableBase\":\"10000\"," +
"\"TaxableReduced\":\"25000.56\",\"VatTotalReducedTax\":\"1000.02\"," +
"\"TaxableIntermediateBase\":\"75000\",\"VatTotalIntermediateTax\":\"6750\"," +
"\"TaxableNormalBase\":\"100000\",\"VatTotalNormalBase\":\"18000\"}," +
"\"KFiscalSpace\":{\"TaxCountryRegion\":\"PT-MA\",\"TaxableBase\":\"5000\"," +
"\"TaxableReduced\":\"12500\",\"VatTotalReducedTax\":\"625\"," +
"\"TaxableIntermediateBase\":\"25000\",\"VatTotalIntermediateTax\":\"3000\"," +
"\"TaxableNormalBase\":\"40000\",\"VatTotalNormalBase\":\"8800\"},\"NotTaxable\":\"100\"," +
"\"StampDuty\":\"25\",\"TaxPayable\":\"64000.02\",\"GrossTotal\":\"513600.58\"," +
"\"WithholdingTaxAmount\":\"100\",\"HashQuartet\":\"kLp0\",\"SWCertNo\":9999," +
"\"OtherInfo\":\"TB;PT00000000000000000000000;513500.58\"}"
var f FatQR
err := f.Scan(input, 0)
if err != nil {
t.Errorf("Scan should've been successful: %+v", err)
}
jsonData := f.ToJSON()
if diff := cmp.Diff(string(jsonData), expected); diff != "" {
t.Errorf("String mismatch (-want +got):\n%s", diff)
}
var newF FatQR
err = newF.FromJSON(jsonData)
if err != nil {
t.Errorf("FromJSON should've been successful: %+v", err)
}
newJsonData := newF.ToJSON()
if diff := cmp.Diff(string(newJsonData), expected); diff != "" {
t.Errorf("String mismatch (-want +got):\n%s", diff)
}
}
// TODO: Add more UTs