package domain import ( "encoding/base64" "fmt" "net/url" "strings" ) const ( versionCodeMinLength = 2 versionCodeMaxLength = 2 ) type Version struct { code string name string host string timezone string } var versionModelName = "Version" // UnmarshalVersionFromDatabase unmarshals Version from the database. // // It should be used only for unmarshalling from the database! // You can't use UnmarshalVersionFromDatabase as constructor - It may put domain into the invalid state! func UnmarshalVersionFromDatabase( code string, name string, host string, timezone string, ) (Version, error) { if code == "" { return Version{}, ValidationError{ Model: versionModelName, Field: "code", Err: ErrRequired, } } if name == "" { return Version{}, ValidationError{ Model: versionModelName, Field: "name", Err: ErrRequired, } } if host == "" { return Version{}, ValidationError{ Model: versionModelName, Field: "host", Err: ErrRequired, } } if timezone == "" { return Version{}, ValidationError{ Model: versionModelName, Field: "timezone", Err: ErrRequired, } } return Version{ code: code, name: name, host: host, timezone: timezone, }, nil } func (v Version) Code() string { return v.code } func (v Version) Name() string { return v.name } func (v Version) Host() string { return v.host } func (v Version) Timezone() string { return v.timezone } func (v Version) URL() *url.URL { return &url.URL{ Scheme: "https", Host: v.host, } } func (v Version) IsZero() bool { return v == Version{} } type Versions []Version type VersionSort uint8 const ( VersionSortCodeASC VersionSort = iota + 1 VersionSortCodeDESC ) type VersionCursor struct { code NullString } const versionCursorModelName = "VersionCursor" func NewVersionCursor(code NullString) (VersionCursor, error) { if !code.Valid { return VersionCursor{}, nil } if err := validateVersionCode(code.Value); err != nil { return VersionCursor{}, ValidationError{ Model: versionCursorModelName, Field: "code", Err: err, } } return VersionCursor{ code: code, }, nil } const ( encodedVersionCursorMinLength = 1 encodedVersionCursorMaxLength = 1000 ) func decodeVersionCursor(encoded string) (VersionCursor, error) { if err := validateStringLen(encoded, encodedVersionCursorMinLength, encodedVersionCursorMaxLength); err != nil { return VersionCursor{}, err } decodedBytes, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return VersionCursor{}, ErrInvalidCursor } decoded := string(decodedBytes) code, ok := strings.CutPrefix(decoded, "code=") if !ok { return VersionCursor{}, ErrInvalidCursor } vc, err := NewVersionCursor(NullString{ Value: code, Valid: true, }) if err != nil { return VersionCursor{}, ErrInvalidCursor } return vc, nil } func (vc VersionCursor) Code() NullString { return vc.code } func (vc VersionCursor) IsZero() bool { return !vc.code.Valid } func (vc VersionCursor) Encode() string { if vc.IsZero() { return "" } return base64.StdEncoding.EncodeToString([]byte("code=" + vc.code.Value)) } type ListVersionsParams struct { codes []string sort []VersionSort cursor VersionCursor limit int } const ( VersionListMaxLimit = 200 listVersionsParamsModelName = "ListVersionsParams" ) func NewListVersionsParams() ListVersionsParams { return ListVersionsParams{ sort: []VersionSort{VersionSortCodeASC}, limit: VersionListMaxLimit, } } func (params *ListVersionsParams) Codes() []string { return params.codes } func (params *ListVersionsParams) SetCodes(codes []string) error { params.codes = codes return nil } const ( versionSortMinLength = 1 versionSortMaxLength = 1 ) func (params *ListVersionsParams) Sort() []VersionSort { return params.sort } func (params *ListVersionsParams) SetSort(sort []VersionSort) error { if err := validateSliceLen(sort, versionSortMinLength, versionSortMaxLength); err != nil { return ValidationError{ Model: listVersionsParamsModelName, Field: "sort", Err: err, } } params.sort = sort return nil } func (params *ListVersionsParams) Cursor() VersionCursor { return params.cursor } func (params *ListVersionsParams) SetCursor(vc VersionCursor) error { params.cursor = vc return nil } func (params *ListVersionsParams) SetEncodedCursor(encoded string) error { decoded, err := decodeVersionCursor(encoded) if err != nil { return ValidationError{ Model: listVersionsParamsModelName, Field: "cursor", Err: err, } } params.cursor = decoded return nil } func (params *ListVersionsParams) Limit() int { return params.limit } func (params *ListVersionsParams) SetLimit(limit int) error { if err := validateIntInRange(limit, 1, VersionListMaxLimit); err != nil { return ValidationError{ Model: listVersionsParamsModelName, Field: "limit", Err: err, } } params.limit = limit return nil } type ListVersionsResult struct { versions Versions self VersionCursor next VersionCursor } const listVersionsResultModelName = "ListVersionsResult" func NewListVersionsResult(versions Versions, next Version) (ListVersionsResult, error) { var err error res := ListVersionsResult{ versions: versions, } if len(versions) > 0 { res.self, err = NewVersionCursor(NullString{ Value: versions[0].Code(), Valid: true, }) if err != nil { return ListVersionsResult{}, ValidationError{ Model: listVersionsResultModelName, Field: "self", Err: err, } } } res.next, err = NewVersionCursor(NullString{ Value: next.Code(), Valid: !next.IsZero(), }) if err != nil { return ListVersionsResult{}, ValidationError{ Model: listVersionsResultModelName, Field: "next", Err: err, } } return res, nil } func (res ListVersionsResult) Versions() Versions { return res.versions } func (res ListVersionsResult) Self() VersionCursor { return res.self } func (res ListVersionsResult) Next() VersionCursor { return res.next } type VersionNotFoundError struct { VersionCode string } func (e VersionNotFoundError) Error() string { return fmt.Sprintf("version with code %s not found", e.VersionCode) } func (e VersionNotFoundError) Type() ErrorType { return ErrorTypeNotFound } func (e VersionNotFoundError) Code() string { return "version-not-found" } func (e VersionNotFoundError) Params() map[string]any { return map[string]any{ "Code": e.VersionCode, } }