TSFoster / elm-uuid / UUID

A UUID looks something like e1631449-6321-4a58-920c-5440029b092e, and can be used as an identifier for anything. Each 128-bit number, usually represented as 32 hexadecimal digits, is generally used under the assumption that it is Universally Unique (hence the name).

This package supports variant 1 UUIDs (those outlined in RFC 4122), which covers the vast majority of those in use (versions 1-5).


type UUID

This modules provides a UUID type, and functions to work with them. It is an opaque type, which basically means you have to use the provided functions if you want to do anything with it!

Here is an example of a Book model, which uses UUIDs to identify both the book and its authors:

type alias Book =
    { title : String
    , uuid : UUID
    , published : Maybe Date
    , authors : List UUID
    }

Reading UUIDs

fromString : String -> Result Error UUID

You can attempt to create a UUID from a string. This function can interpret a fairly broad range of formatted (and mis-formatted) UUIDs, including ones with too much whitespace, too many (or not enough) hyphens, or uppercase characters.

fromString "c72c207b-0847-386d-bdbc-2e5def81cf811"
--> Err WrongLength

fromString "c72c207b-0847-386d-bdbc-2e5def81cg81"
--> Err WrongFormat

fromString "c72c207b-0847-386d-bdbc-2e5def81cf81"
    |> Result.map version
--> Ok 3

fromString "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
--> Ok dnsNamespace


type Error
    = WrongFormat
    | WrongLength
    | UnsupportedVariant
    | IsNil
    | NoVersion

This enumerates all the possible errors that can happen when reading existing UUIDs.

import Bytes.Extra exposing (fromByteValues)

-- Note the non-hexadecimal characters in this string!
fromString "12345678-9abc-4567-89ab-cdefghijklmn"
--> Err WrongFormat

fromBytes (fromByteValues [ 223 ])
--> Err WrongLength

-- This package only supports variant 1 UUIDs
fromString "urn:uuid:12345678-9abc-4567-c234-abcd12345678"
--> Err UnsupportedVariant

fromBytes (fromByteValues [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
--> Err IsNil

fromString "{00000000-0000-0000-0000-000000000000}"
--> Err IsNil

fromString "6ba7b814-9dad-71d1-80b4-00c04fd430c8"
--> Err NoVersion

Creating UUIDs

Random UUIDs (Version 4)

generator : Random.Generator UUID

Generating a random UUID (version 4) is, I think, the most straightforward way of making a UUID, and I see them used all the time. There are a couple of ways of using a generator to create a value, which are described nicely in the elm/random docs.

NOTE: Random.Seeds have either 32 or 54 bits of randomness. That means that UUIDs generated with this Generator will only ever be one of either 4.3 billion or 18 quadrillion UUIDs. If that is not enough for your use case, check out step. It is also recommended that you do not use Random.generate.

import Random

Random.step UUID.generator (Random.initialSeed 12345)
    |> Tuple.first
    |> UUID.toRepresentation Urn
--> "urn:uuid:5b58931d-bb69-406d-81a9-7746c300838c"


type alias Seeds =
{ seed1 : Random.Seed
, seed2 : Random.Seed
, seed3 : Random.Seed
, seed4 : Random.Seed 
}

step : Seeds -> ( UUID, Seeds )

Properly using all of a randomly-generated UUID’s bits requires up to four Random.Seeds. How you generate them is up to you. You might want to check out Crypto.getRandomValues() though.

Use four seeds to create a version 4 UUID and four new seeds.

    import Random

    s1 = Random.initialSeed 3684687
    s2 = Random.initialSeed 3487532
    s3 = Random.initialSeed 63374
    s4 = Random.initialSeed 65483

    step (Seeds s1 s2 s3 s4)
      |> Tuple.first
      |> toString

    ---> "3d938336-567b-43b9-88d5-3703c2e63a0b"

Hierarchical, namespaced UUIDs (Version 3, Version 5)

UUIDs can be created using a namespace UUID and a name, which is then hashed to create a new UUID. The hash function used depends on the version of UUID: verison 3 UUIDs use MD5, and version 5 UUIDs use SHA-1. Version 5 is the officially recommended version to use.

Although the RFC defining UUIDs defines some base namespaces, any UUID can be used as a namespace, making a hierarchy. I think this is pretty cool! You can use this method for consistently making the same UUID from the same data, which can be very useful in some situations.

forName : String -> UUID -> UUID

Create a version 5 UUID from a String and a namespace, which should be a UUID. The same name and namespace will always produce the same UUID, which can be used to your advantage. Furthermore, the UUID created from this can be used as a namespace for another UUID, creating a hierarchy of sorts.

apiNamespace : UUID
apiNamespace =
    forName "https://api.example.com/v2/" dnsNamespace

widgetNamespace : UUID
widgetNamespace =
    forName "Widget" apiNamespace


toString apiNamespace
--> "bad122ad-b5b6-527c-b544-4406328d8b13"

toString widgetNamespace
--> "7b0db628-d793-550b-a883-937a276f4908"

forBytes : Bytes -> UUID -> UUID

Create a version 5 UUID from Bytes and a namespace, which should be a UUID. The same name and namespace will always produce the same UUID, which can be used to your advantage. Furthermore, the UUID created from this can be used as a namespace for another UUID, creating a hierarchy of sorts.

import Bytes
import Bytes.Encode

type alias Widget = { name: String, value: Float }

apiNamespace : UUID
apiNamespace =
    forName "https://api.example.com/v2/" dnsNamespace

widgetNamespace : UUID
widgetNamespace =
    forName "Widget" apiNamespace

uuidForWidget : Widget -> UUID
uuidForWidget { name, value } =
    let
       bytes =
         Bytes.Encode.encode <| Bytes.Encode.sequence
            [ Bytes.Encode.unsignedInt32 Bytes.BE (String.length name)
            , Bytes.Encode.string name
            , Bytes.Encode.float64 Bytes.BE value
            ]
    in
    forBytes bytes widgetNamespace


Widget "Exponent" 0.233
    |> uuidForWidget
    |> toString
--> "46dab34b-5447-5c5c-8e24-fe4e3daab014"

forNameV3 : String -> UUID -> UUID

Similar to forName, creates a UUID based on the given String and namespace UUID, but creates a version 3 UUID. If you can choose between the two, it is recommended that you choose version 5 UUIDs.

apiNamespace : UUID
apiNamespace =
    forNameV3 "https://api.example.com/v1/" dnsNamespace

toString apiNamespace
--> "f72dd2ff-b8e7-3c58-afe1-352d5e273de4"

forBytesV3 : Bytes -> UUID -> UUID

Similar to forBytes, creates a UUID based on the given Bytes and namespace UUID, but creates a version 3 UUID. If you can choose between the two, it is recommended that you choose version 5 UUIDs.

import Bytes
import Bytes.Encode

apiNamespace : UUID
apiNamespace =
    forName "https://api.example.com/v2/" dnsNamespace

forBytesV3 (Bytes.Encode.encode (Bytes.Encode.unsignedInt8 245)) apiNamespace
    |> toString
--> "4ad1f4bf-cf83-3c1d-b4f5-975f7d0fc9ba"

Officially-recognized namespaces

dnsNamespace : UUID

A UUID for the DNS namespace, "6ba7b810-9dad-11d1-80b4-00c04fd430c8".

forName "elm-lang.org" dnsNamespace
    |> toString
--> "c6d62c23-3406-5fc7-836e-9d6bef13e18c"

urlNamespace : UUID

A UUID for the URL namespace, "6ba7b811-9dad-11d1-80b4-00c04fd430c8".

forName "https://package.elm-lang.org" urlNamespace
    |> toString
--> "e1dba7a5-c338-53f3-bc90-353f045447be"

oidNamespace : UUID

A UUID for the ISO object ID (OID) namespace, "6ba7b812-9dad-11d1-80b4-00c04fd430c8".

forName "1.2.250.1" oidNamespace
    |> toString
--> "87a2ac60-306f-5131-b748-3787f9f55685"

x500Namespace : UUID

A UUID for the X.500 Distinguished Name (DN) namespace, "6ba7b814-9dad-11d1-80b4-00c04fd430c8".

If you don't know what this is for, I can't help you, because I don't know either.

Binary representation

fromBytes : Bytes -> Result Error UUID

You can attempt to create a UUID from some Bytes, the only contents of which much be the UUID.

import Bytes.Extra exposing (fromByteValues)

fromBytes (fromByteValues [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
--> Err IsNil

fromBytes (fromByteValues [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
--> Err WrongLength

fromBytes (fromByteValues [0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8])
--> Ok x500Namespace

decoder : Bytes.Decode.Decoder UUID

A Bytes.Decode.Decoder, for integrating with a broader decode.

import Bytes
import Bytes.Decode exposing (Decoder)
import Bytes.Extra exposing (fromByteValues)

type alias Record =
    { name : String
    , value : Int
    , id : UUID
    }

recordDecoder : Decoder Record
recordDecoder =
    Bytes.Decode.map3 Record
        (Bytes.Decode.unsignedInt16 Bytes.BE |> Bytes.Decode.andThen Bytes.Decode.string)
        (Bytes.Decode.unsignedInt32 Bytes.BE)
        UUID.decoder

Bytes.Decode.decode recordDecoder <| fromByteValues
    [ 0x00, 0x0b -- 11
    , 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64 -- "hello world"
    , 0x00, 0x00, 0x00, 0xFF -- 255
    , 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8 -- UUID
    ]
--> Just
-->     { name = "hello world"
-->     , value = 255
-->     , id = urlNamespace
-->     }

toBytes : UUID -> Bytes

Convert a UUID to Bytes.

import Bytes.Extra exposing (toByteValues)

toBytes dnsNamespace
    |> toByteValues
--> [0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8]

encoder : UUID -> Bytes.Encode.Encoder

An encoder for encoding data that includes a UUID.

import Bytes.Extra exposing (toByteValues)
import Bytes.Encode
import Bytes.Encode.Extra

defaultNamespaces : List UUID
defaultNamespaces =
    [ dnsNamespace, urlNamespace, oidNamespace, x500Namespace ]

Bytes.Encode.Extra.list UUID.encoder defaultNamespaces
    |> Bytes.Encode.encode
    |> toByteValues
--> [ 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8
--> , 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8
--> , 0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8
--> , 0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8
--> ]

Formatting UUIDs

UUIDs are generally represented by 32 hexadecimal digits in the form 00112233-4455-M677-N899-aabbccddeeff, where the four bits at position M denote the UUID version, and the first two or three bits at position N denote the variant. This is the "canonical" representation, but there is also a URN representation and a format used mostly by Microsoft, where they are more commonly named GUIDs. Additionally, some APIs do not use the dashes.

toString : UUID -> String

The canonical representation of the UUID. This is the same as toRepresentation Canonical.

toString dnsNamespace
--> "6ba7b810-9dad-11d1-80b4-00c04fd430c8"

toRepresentation : Representation -> UUID -> String

Convert UUID to a given format

toRepresentation Canonical dnsNamespace
--> "6ba7b810-9dad-11d1-80b4-00c04fd430c8"

toRepresentation Urn dnsNamespace
--> "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"

toRepresentation Guid dnsNamespace
--> "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}"

toRepresentation Compact dnsNamespace
--> "6ba7b8109dad11d180b400c04fd430c8"


type Representation
    = Canonical
    | Urn
    | Guid
    | Compact

There are three typical human-readable representations of UUID: canonical, a Uniform Resource Name and Microsoft's formatting for its GUIDs (which are just version 4 UUIDs nowadays). Additionally, some APIs do not use the dashes, which has been named Compact here.

JSON

jsonDecoder : Json.Decode.Decoder UUID

Decode a UUID from a JSON string.

toValue : UUID -> Json.Encode.Value

Encode a UUID to a JSON Value (as a canonical string).

toValueWithRepresentation : Representation -> UUID -> Json.Encode.Value

Encode a UUID to a JSON Value (as a string with a given (Representation)[#Representation]).

Inspecting UUIDs

version : UUID -> Basics.Int

Get the version number of a UUID. Only versions 3, 4 and 5 are supported in this package, so you should only expect the returned Int to be 3, 4 or 5.

import Random

Random.step generator (Random.initialSeed 1)
    |> Tuple.first
    |> version
--> 4

fromString "{12345678-1234-5678-8888-0123456789ab}"
    |> Result.map version
--> Ok 5

compare : UUID -> UUID -> Basics.Order

Returns the relative ordering of two UUIDs. The main use case of this function is helping in binary-searching algorithms. Mimics elm/core's compare.

The Nil UUID

One more, special UUID is the nil UUID. This is just "00000000-0000-0000-0000-000000000000". I suppose it can work as a placeholder in some cases, but elm has Maybe for that! Nevertheless, you might need to check for nil UUIDs, or print the nil UUID.

nilBytes : Bytes

128 bits of nothing.

isNilBytes : Bytes -> Basics.Bool

True if the given bytes are the nil UUID (00000000-0000-0000-0000-000000000000).

isNilBytes nilBytes
--> True

nilString : String

nilString
--> "00000000-0000-0000-0000-000000000000"

isNilString : String -> Basics.Bool

True if the given string represents the nil UUID (00000000-0000-0000-0000-000000000000).

nilRepresentation : Representation -> String

nilRepresentation Canonical
--> "00000000-0000-0000-0000-000000000000"

nilRepresentation Urn
--> "urn:uuid:00000000-0000-0000-0000-000000000000"

nilRepresentation Guid
--> "{00000000-0000-0000-0000-000000000000}"