Skip to content

Commit

Permalink
Implement typeid cli, and add FromX constructors (#25)
Browse files Browse the repository at this point in the history
This is the last typeid PR before I start sharing it publicly.

It implements a `typeid` cli with three commands `new`, `encode`, and
`decode`. Which let you generate a new typeid, encode a uuid as a
typeid, and decode a typeid as a uuid.

In the process add some `FromX` constructors in the `go` version of the
library (which brings it to parity with our internal typescript version)
  • Loading branch information
loreto authored Jun 14, 2023
1 parent 9885c57 commit 4c5517b
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 19 deletions.
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ TypeIDs are a modern, type-safe extension of UUIDv7.
TypeIDs are canonically encoded as lowercase strings consisting of three parts:
1. A type prefix
2. An underscore '_' separator
3. A 128-bit UUIDv7 encoded as a 26-character string in base32.
3. A 128-bit UUIDv7 encoded as a 26-character string in base32 (using [Crockford's alphabet](https://www.crockford.com/base32.html) in lowercase).

Here's an example of a TypeID of type `user`:

```
user_2x4y6z8a0b1c2d3e4f5g6h7j8k
└──┘ └────────────────────────┘
type uuid (base32)
type uuid suffix (base32)
```

## Benefits
+ **Type-safe:** you can't accidentally use a `user` ID where a `post` ID is expected. When debugging, you can
immediately understand what type of entity a TypeID refers to thanks to the type prefix.
+ **Compatible with UUIDs:** TypeIDs are a superset of UUIDs. They are based on the upcoming [UUIDv7 standard](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7). If you decode the TypeID and remove the type information, you get a valid UUIDv7.
+ They are K-sortable and can be used as the primary key in a database. Entirely random global ids generally
suffer from poor database locality.
+ Thoughtful encoding: the base32 encoding is URL safe, case-insensitive, avoids ambiguous characters, can be
+ **K-Sortable**: TypeIDs are K-sortable and can be used as the primary key in a database while ensuring good
locality. Compare to entirely random global ids, like UUIDv4, that generally suffer from poor database locality.
+ **Thoughtful encoding**: the base32 encoding is URL safe, case-insensitive, avoids ambiguous characters, can be
selected for copy-pasting by double-clicking, and is a more compact encoding than the traditional hex encoding used by UUIDs (26 characters vs 36 characters).

## Implementations
Expand All @@ -39,6 +39,36 @@ Here's an example of a TypeID of type `user`:
| TypeScript | ... Coming Soon |

We are looking for community contributions to implement TypeIDs in other languages.

## Command-line Tool
This repo includes a command-line tool for generating TypeIDs. To install it, run:

```
go install github.com/jetpack-io/typeid
```

To generate a new TypeID, run:

```
$ typeid prefix
prefix_01h2xcejqtf2nbrexx3vqjhp41
```

To decode an existing TypeID into a UUID run:

```
$ typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41
type: prefix
uuid: 0188bac7-4afa-78aa-bc3b-bd1eef28d881
```

And to encode an existing UUID into a TypeID run:

```
$ typeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881
prefix_01h2xcejqtf2nbrexx3vqjhp41
```

## Related Work
+ [UUIDv7](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7) - The upcoming UUID standard that TypeIDs are based on.

Expand Down
29 changes: 29 additions & 0 deletions cli/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cli

import (
"github.com/spf13/cobra"
"go.jetpack.io/typeid"
)

func DecodeCmd() *cobra.Command {
command := &cobra.Command{
Use: "decode <type_id>",
Args: cobra.ExactArgs(1),
Short: "Decode the given TypeID into a UUID",
RunE: decodeCmd,
SilenceErrors: true,
SilenceUsage: true,
}

return command
}

func decodeCmd(cmd *cobra.Command, args []string) error {
tid, err := typeid.FromString(args[0])
if err != nil {
return err
}
cmd.Printf("type: %s\n", tid.Type())
cmd.Printf("uuid: %s\n", tid.UUID())
return nil
}
36 changes: 36 additions & 0 deletions cli/encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cli

import (
"github.com/spf13/cobra"
"go.jetpack.io/typeid"
)

func EncodeCmd() *cobra.Command {
command := &cobra.Command{
Use: "encode [<type_prefix>] <uuid>",
Args: cobra.RangeArgs(1, 2),
Short: "Encode the given UUID into a TypeID using the given type prefix",
RunE: encodeCmd,
SilenceErrors: true,
SilenceUsage: true,
}

return command
}

func encodeCmd(cmd *cobra.Command, args []string) error {
prefix := ""
uuid := ""
if len(args) == 1 {
uuid = args[0]
} else {
prefix = args[0]
uuid = args[1]
}
tid, err := typeid.FromUUID(prefix, uuid)
if err != nil {
return err
}
cmd.Println(tid)
return nil
}
34 changes: 34 additions & 0 deletions cli/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cli

import (
"strings"

"github.com/spf13/cobra"
"go.jetpack.io/typeid"
)

func NewCmd() *cobra.Command {
command := &cobra.Command{
Use: "new [<type_prefix>]",
Args: cobra.MaximumNArgs(1),
Short: "Generate a new TypeID using the given type prefix",
RunE: newCmd,
SilenceErrors: true,
SilenceUsage: true,
}

return command
}

func newCmd(cmd *cobra.Command, args []string) error {
prefix := ""
if len(args) > 0 {
prefix = strings.ToLower(args[0])
}
tid, err := typeid.New(prefix)
if err != nil {
return err
}
cmd.Println(tid)
return nil
}
42 changes: 42 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cli

import (
"context"
"fmt"
"os"

"github.com/spf13/cobra"
)

func RootCmd() *cobra.Command {
command := &cobra.Command{
Use: "typeid",
Short: "Type-safe, K-sortable, globally unique identifiers",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
SilenceErrors: true,
SilenceUsage: true,
}
command.AddCommand(NewCmd())
command.AddCommand(EncodeCmd())
command.AddCommand(DecodeCmd())

return command
}

func Execute(ctx context.Context, args []string) int {
cmd := RootCmd()
cmd.SetArgs(args)
err := cmd.ExecuteContext(ctx)
if err != nil {
fmt.Printf("[Error] %v\n", err)
return 1
}
return 0
}

func Main() {
code := Execute(context.Background(), os.Args[1:])
os.Exit(code)
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
module go.jetpack.io/typeid-cli

go 1.20

require github.com/spf13/cobra v1.7.0

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
16 changes: 2 additions & 14 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
package main

import (
"fmt"
"os"

"go.jetpack.io/typeid"
)
import "go.jetpack.io/typeid-cli/cli"

func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: typeid [<type_prefix>]")
os.Exit(1)
}

prefix := os.Args[1]
tid := typeid.Must(typeid.New(prefix))
fmt.Println(tid)
cli.Main()
}

0 comments on commit 4c5517b

Please sign in to comment.