TSFoster/elm-uuid/4.3.1

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 9562, previously RFC4122), which covers the vast majority of those in use (versions 1-8).

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-91d1-80b4-00c04fd430c8"
--> Err NoVersion

Creating UUIDs

Random UUIDs (Version 4)

generator : 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 : Seed
, seed2 : Seed
, seed3 : Seed
, seed4 : 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"

Time-based random UUIDs (Version 7)

Version 7 UUIDs contain a Unix timestamp in milliseconds and random data, making them both unique and time-sortable. This is useful when UUIDs are used as database primary keys, as it improves index locality.

generatorV7 : Posix -> Generator UUID

Generate a random version 7 UUID from a given timestamp. Version 7 UUIDs (defined in RFC 9562) contain a 48-bit Unix timestamp in milliseconds and random data, making them time-sortable.

For generating multiple UUIDs within the same millisecond with a guaranteed monotonic order, see stepV7.

import Random import Time

Random.step (UUID.generatorV7 (Time.millisToPosix 1234567890000)) (Random.initialSeed 42)
    |> Tuple.first
    |> UUID.version
--> 7
type V7State

State used by stepV7 to generate monotonically increasing version 7 UUIDs within the same millisecond.

Create an initial V7State with the counter starting at 0, allowing up to 4096 monotonic increments before the 12-bit counter wraps.

import Random


v7State =
    Random.initialSeed 42
        |> Random.step UUID.initialV7State
        |> Tuple.first
stepV7 : Posix -> V7State -> ( UUID, V7State )

Generate a version 7 UUID with a monotonically increasing counter.

The 12-bit counter in the rand_a field ensures monotonic ordering:

  • Same millisecond: the counter increments from its previous value, guaranteeing that up to 4096 UUIDs per millisecond are strictly ordered.
  • Different millisecond: the counter resets to 0, since the timestamp alone is sufficient to order UUIDs across different milliseconds.

Note: if more than 4096 UUIDs are generated within the same millisecond, the 12-bit counter wraps around and monotonicity is no longer guaranteed.

import Random
import Time


time : Time.Posix
time = Time.millisToPosix 1234567890000


result : Basics.Order
result =
    let
        ( state0, _ ) = Random.step UUID.initialV7State (Random.initialSeed 42)
        ( uuid1, state1 ) = UUID.stepV7 time state0
        ( uuid2, _ ) = UUID.stepV7 time state1
    in
        UUID.compare uuid1 uuid2


result
--> LT

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

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

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

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

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

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"

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 : 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 -> 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"

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"
= 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 : Decoder UUID

Decode a UUID from a JSON string.

toValue : UUID -> Value

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

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

Inspecting UUIDs

version : UUID -> Int

Get the version number of a UUID. Versions 1-8 are supported for parsing. This package can create versions 3, 4, 5 and 7.

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 -> 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 -> 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 -> Bool

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

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


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


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