Skip to content
domondaPublic

About

Frequently used Go types in the Domonda App

Resources

Stars

5 stars

Watchers

5 watching

Forks

Latest commit

 

History

764 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-types

Go Reference Go Report Card Go Go version license

Go types for the data that business documents are made of: money amounts and currencies, dates, IBANs and BICs, VAT IDs, country and language codes, email addresses and messages, and UUIDs. Each type parses the many ways people and other systems write a value, normalizes it to one canonical form, validates it, and round-trips through database/sql, encoding/json and encoding/xml. Optional values have Nullable variants that map to SQL NULL and JSON null.

go get github.com/domonda/go-types

Requires Go 1.26 or later.

Quick start

package main

import (
	"fmt"

	"github.com/domonda/go-types/bank"
	"github.com/domonda/go-types/date"
	"github.com/domonda/go-types/language"
	"github.com/domonda/go-types/money"
	"github.com/domonda/go-types/vat"
)

func main() {
	// Normalize identifiers as they appear on invoices
	vatID, err := vat.ID("atu 1022 3006").Normalized()
	fmt.Println(vatID, err)

	iban, err := bank.IBAN("DE89 3704 0044 0532 0130 00").Normalized()
	fmt.Println(iban, err)

	// Dots are day first, slashes month first unless a language hint says otherwise
	d, err := date.Normalize("05.06.2024")
	fmt.Println(d, err)
	d, err = date.Normalize("05/06/2024", language.EN)
	fmt.Println(d, err)

	// Exact money amounts with locale-aware parsing
	amount, err := money.ParseDecimalAmount("1.234,50")
	fmt.Println(amount, err)
	currency, err := money.Currency("€").Normalized()
	fmt.Println(currency, err)

	// Integer cents split without losing or inventing a cent
	fmt.Println(money.CentAmount(1000).SplitEqually(3))

	// Output:
	// ATU10223006 <nil>
	// DE89370400440532013000 <nil>
	// 2024-06-05 <nil>
	// 2024-05-06 <nil>
	// 1234.50 <nil>
	// EUR <nil>
	// [3.33 3.33 3.34]
}

New here? The tutorial builds an invoice record from raw strings to validated JSON and SQL values step by step.

Packages

Package What it gives you
types (root) Set[T], the Validator/Validatable interfaces with Validate and DeepValidate, Normalizable, LenString, KeyMutex, Finder, pointer and iterator helpers
account Alphanumeric account numbers with SQL, JSON and XML support
bank IBAN and BIC validation and normalization, finders for document text, CAMT.053 XML structs
charset BOM detection, UTF-16 and UTF-32, legacy charsets by IANA or MIME label
country ISO 3166-1 alpha-2 country codes, alternative spellings, EU membership
date ISO 8601 Date, YearMonth, YearQuarter, lenient locale-aware parsing, period ranges
email Lenient address parsing, address lists and sets, MIME and TNEF message parsing and building
float Locale-aware float parsing and formatting, decimal rounding, a JSON-tolerant float
language ISO 639-1 language codes from BCP 47, ISO 639-2/3 and locale identifiers
mapset Set algebra on map[K]struct{} and map[K]bool, mirroring the proposed container/mapset
money Amount (float64), CentAmount (int64 cents), DecimalAmount (exact fixed-point), ISO 4217 currencies, rates
notnull Never-null SQL/JSON wrappers: nil slices become '{}' and []
nullable Nullable wrappers that map the zero value to SQL NULL and JSON null, generic Type[T]
queue Unbounded goroutine-safe FIFO queue consumed through a channel
set Helpers for map[T]struct{} sets (prefer mapset for new code)
strfmt Reflection-driven scanning and formatting of values with locale presets
strutil Trimming, rune filtering, transliteration, file names, domain extraction, StringSet
strutil/stringfrom Render pointers and optional values as strings with a text for nil
uu UUID ID, NullableID, IDSlice, IDSet, v7 by default, context-injectable generation
vat European VAT IDs with per-country format and checksum validation

Every package README is the complete reference for its package: all exported types and functions, how each value is written to and read from SQL and JSON, the gotchas, and runnable examples. The same API is on pkg.go.dev.

Documentation

The docs index lists everything. By what you need:

Conventions shared by all packages

  • Identifier types like country.Code, vat.ID, bank.IBAN and uu.ID have an invalid zero value, and their NullableX variant uses that zero value as SQL NULL and JSON null. Number types like money.Amount, where zero is a valid value, use the generic nullable.Type[T] instead. See null conventions.
  • Valid() bool and Validate() error accept the input that Normalized() accepts, so country.Code("de").Valid() and vat.ID("atu 1022 3006").Valid() are true. ValidAndNormalized() reports whether a value is already in canonical form. See validation and normalization.
  • Reading and writing trim, and uppercase IBAN, BIC, VAT, currency and year-quarter. Country codes, language codes, email addresses and year-months are only trimmed. Value() writes an invalid value back. date.Date on SQL still fully normalizes. fmt prints the stored value, see Reading and writing values. How each type is written to and read from SQL, JSON and XML, including its zero and null values, is in the Serialization section of its package README.
  • types.Validate and types.DeepValidate work with any type that implements Validate() error or Valid() bool, including your own.

Reading and writing values

SQL, JSON, XML and text only prettify a value. They trim surrounding whitespace, and uppercase letters where the type is written in upper case: bank.IBAN, bank.BIC, vat.ID, money.Currency, date.YearQuarter. country.Code, language.Code, email.Address and date.YearMonth are only trimmed, so "Deutschland" stays "Deutschland" and "EN" stays "EN". An invalid value is written back after that prettification. Value does not refuse it. Normalized() is unchanged and is what ScanString(s, true) stores. Printing shows what is stored.

date.Date is the exception on SQL. Scan and Value fully normalize, because a database date is not free text. A time.Time from the driver is stored with OfTime, which is already YYYY-MM-DD. JSON and XML only trim.

Methods per format

Format Read Write
SQL, database/sql Scan Value
JSON, encoding/json UnmarshalJSON MarshalJSON
XML, encoding/xml UnmarshalXML, UnmarshalXMLAttr MarshalText, MarshalXMLAttr
Text, strfmt and CSV ScanString MarshalText
JSON object keys of a map as given as given
fmt, templates, logs as stored

Reading

Input Scan, JSON, XML, ScanString(s, false) ScanString(s, true)
"DE" "DE" "DE"
" de " "de" "DE"
"Deutschland" "Deutschland" "DE"
"XX" "XX", no error error
"", " " "" Code: error, NullableCode: null
SQL NULL ""
JSON null Code: unchanged, NullableCode: ""
JSON number, SQL integer error

For date.Date and date.NullableDate, SQL Scan and Value treat the zero dates 0001-01-01 and 0000-00-00, and spellings of them like 01.01.0001, as blank. JSON and XML only trim, so those spellings stay. A time.Time from the driver is stored with OfTime, and the zero time.Time as "". date.YearMonth and date.YearQuarter read a time.Time as its month or quarter the same way, and keep a zero value such as "0001-01" or "0001-Q1" when it is already text.

Reading never fails because of the text itself. An invalid value is kept after prettification, so one bad row doesn't fail a whole query or JSON document, and types.DeepValidate reports it together with its field path.

Writing

Value in Go Value (SQL) MarshalJSON MarshalText, XML fmt, String
"DE" DE "DE" DE DE
"de", converted in Go de "de" de de
"XX" XX "XX" XX XX
"" NULL Code: "", NullableCode: null empty empty

fmt, templates and String show the value as it is stored. Normalized() is the canonical spelling, and it is not applied while reading or writing.

XML writes <country>DE</country> for an element and country="DE" for an attribute field. A nil *country.Code attribute field is left out, with or without omitempty. A nil pointer ,chardata field still makes encoding/xml panic, like a nil *time.Time, because encoding/xml calls MarshalText through the nil pointer and has no hook to skip it; use a non-pointer ,chardata field.

Differences between types

Types Difference
country.Code, money.Currency, language.Code String() is generated by go-enum and returns the stored value
date.YearMonth, date.YearQuarter and their nullable types String() returns the stored value
vat.ID, bank.IBAN, bank.BIC, date.Date, email.Address no String() method, fmt prints the stored value
NullableX variants blank is null; StringOr(alt) returns alt only for null; ScanString also reads NULL, null and nil as null
language.Code no nullable variant, its "" is null
email.AddressList, email.NullableAddressList the list string is trimmed; addresses are not parsed or lowercased; Value does not reject an invalid list; normalize with Normalized()
email.AddressSet, types.Set[T] keys as stored, so {"a@x.com", "A@x.com"} and a padded key stay distinct; AddressSet writes a PostgreSQL text[] array in SQL, a sorted array as JSON, and an address list as text and XML; MarshalText refuses a key that would not read back as one list entry; normalize with Normalized() or NormalizedAddressList()
Scan and UnmarshalJSON of the email collections read a PostgreSQL array, a JSON array, a JSON object with the addresses as keys, a JSON string and a plain address list
money.CurrencyAmount, money.CurrencyDecimalAmount currency trimmed and uppercased in Value and JSON, as stored in String
notnull.TrimmedString, nullable.TrimmedString trimmed when read and in String
notnull.TrimmedString, nullable.TrimmedString, account.Number, account.NullableNumber encoding/json reads the keys of a map of them trimmed, so " a" and "a" merge into one key

A set writes its elements as given because normalizing could merge two of them, and the set would come back with fewer elements than it was written with. For the same reason the keys of a map[country.Code]V are read and written as given in JSON. The normalizing types have no UnmarshalText for that reason: encoding/json decodes a map key of a type with UnmarshalText through it, and in Go 1.26 through its UnmarshalJSON. The trimming string types and account.Number keep UnmarshalText, because trimming is what they are for and flag.TextVar and other text decoders need it, so their map keys are trimmed.

Design decisions

These rules decide how every type behaves, and how an inconsistency between types gets resolved. Types that don't follow a rule yet are tracked in TODOS.md.

  • Be generous in accepting input, and do not rewrite stored data. Normalized() and Validate() accept every common spelling: country.Code("de"), vat.ID("atu 1022 3006") and money.Currency("€") are valid input. Reading and writing do not apply that canonical form. They only trim, and uppercase where the type is written in upper case, so a value already in the database is not replaced by a stricter spelling. date.Date on SQL is the exception, and uu.ID stays fully canonical. Call Normalized() when a comparison or a map key needs the canonical form. The email types have an extra ASCII() for message headers.
  • Show what is stored. fmt, templates and String print a value as it is stored, never normalized, so a log can't show a non-canonical or invalid value as if it were the canonical one.
  • Collections keep their elements as given. types.Set and email.AddressSet read and write their keys as stored, because normalizing could merge two elements, and a set would read back with fewer elements than it was written with. An address list trims the list string and does not parse or lowercase the addresses. Value does not reject an invalid list. Normalizing them is an explicit call.
  • Empty input is null. For a nullable type, an empty or whitespace-only string from SQL, JSON or text is null, because an empty CSV cell or text column means "no value". A non-nullable type reads it as its empty value, which is invalid, and writes that as SQL NULL, so a NOT NULL column rejects it.
  • Fail loudly instead of guessing. Input that contradicts itself, or whose meaning depends on something the package can't know, is an error instead of a silently wrong value. A number with an ordinal suffix is never read as the month, so "13 1st 2024" is an error, and strfmt rejects a time zone abbreviation whose offset is unknown on the machine instead of reading the time as UTC. A documented default is no guess: an ambiguous date like 05.06.2024 is day first with dots, and 05/06/2024 is month first with slashes unless a language hint says otherwise.
  • Errors are created with github.com/domonda/go-errs (errs.New, errs.Errorf) instead of errors.New and fmt.Errorf.

Development

go test ./...          # all tests
./run-go-enum.sh       # regenerate Enums()/EnumStrings() after adding a code
./run-gosec.sh         # static security analysis

go-enum and gosec are pinned as tool dependencies in tools/go.mod, so the main module does not depend on them. CI builds, vets, checks that the generated enum methods are up to date and runs the tests with the race detector on every pull request.

Status

No version has been tagged yet. CHANGELOG.md lists every change, including the breaking ones, and TODOS.md lists what is left before v1.0.0.

License

MIT, see LICENSE. The uu package is derived from github.com/satori/go.uuid.

About

Frequently used Go types in the Domonda App

Resources

Stars

5 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages