TSFoster/elm-uuid/2.2.1

NOTE: the latest version is 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 assumed to be Universally Unique (hence the name).

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
    }

Creating UUIDs

UUIDs have version numbers (1-5), which describe how they were created, and variant numbers (1-2) which describes how they are stored. This module can read all UUIDs, but can only currently create versions 3, 5 (namespaced, heirarchical systems) and 4 (a randomly-generated UUID). It creates variant 1 UUIDs (probably the best choice if you don't have a specific need for variant 2), but can convert them to variant 2.

Nil UUID

One type of UUID not defined above is the nil UUID. This is a "special form of UUID" defined as "00000000-0000-0000-0000-000000000000". I suppose it can be used as a placeholder for something that doesn't have a UUID yet?

Random UUIDs (Version 4)

Randomly-generated UUIDs are called version 4 UUIDs. This package provides a Random.Generator for the elm/random library.

generator : Generator UUID

Generating a random UUID 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. Here is an example of how you might use the UUID generator:

import Random

type Comment
    = Comment String UUID


makeComment : String -> Random.Seed -> ( Comment, Random.Seed )
makeComment comment seed =
    UUID.generator
        |> Random.map (Comment comment)
        |> Random.step seed

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 (using SHA-1) is the recommended version to use.

Once generated, these UUIDs can then be used as a namespace, making a hierarchy! I think this is pretty cool! You can use this method for making predictable UUIDs from data, and it also has the added bonus that you don't have to deal with random generators/seeds.

childNamed : String -> UUID -> UUID

Start with an existing UUID as a "parent" UUID, and provide a name to create a new UUID.

grandparent : UUID
grandparent = nil


parent : UUID
parent = grandparent |> childNamed "parent"


parentsSibling : UUID
parentsSibling = grandparent |> childNamed "parent's sibling"


child1 : UUID
child1 = parent |> childNamed "child1"


child2 : UUID
child2 = parent |> childNamed "child2"


cousin : UUID
cousin = parentsSibling |> childNamed "cousin"


canonical child2
--> "8081b7b9-1fb0-54a5-bcf3-e27b4b07f381"
v5ChildNamed : String -> UUID -> UUID

Alias for childNamed, for parity with v3ChildNamed, and just in case you need to make extra-clear which version UUID you are using!

v3ChildNamed : String -> UUID -> UUID

Start with an existing UUID as a "parent" UUID, and provide a name to create a new UUID. Note: unless you have good reason to use version 3 UUIDs, it is recommended you create version 5 UUIDs with childNamed.

grandparent : UUID grandparent = nil

parent : UUID
parent = grandparent |> v3ChildNamed "parent"


parentsSibling : UUID
parentsSibling = grandparent |> v3ChildNamed "parent's sibling"


child1 : UUID
child1 = parent |> v3ChildNamed "child1"


child2 : UUID
child2 = parent |> v3ChildNamed "child2"


cousin : UUID
cousin = parentsSibling |> v3ChildNamed "cousin"


canonical child2
--> "4cacaf93-fcc5-3a02-bc41-c0a3e359e11d"

The RFC defining UUIDs defines some base UUIDs to start your hierarchy.

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

UUID.dns |> v3ChildNamed "elm-lang.org" |> UUID.canonical
--> "bb125218-fead-3b3f-bcc5-05fbbe879f4d"

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

UUID.url |> v3ChildNamed "https://package.elm-lang.org" |> UUID.canonical
--> "2035dad9-ee50-3732-a2a5-d76a84f42a8a"

A UUID for the ISO object ID (OID) namespace, "6ba7b812-9dad-11d1-80b4-00c04fd430c8". I am not going to try to explain what an OID is, if you need to use this, you probably have a better grasp of them than I do!

UUID.oid |> v3ChildNamed "1.2.250.1" |> UUID.canonical --> "2eab101c-060d-3424-aad5-8805218a67bc"

A UUID for the X.500 Distinguished Name (DN) namespace, "6ba7b814-9dad-11d1-80b4-00c04fd430c8". I am not going to try to explain what a X.500 DN is, if you need to use this, you probably have a better grasp of them than I do!

Making variant 2 UUIDs

The above functions for creating UUIDs all create variant 1 UUIDs. If you need to create a variant 2 UUID, any UUID can be converted to a variant 2 UUID with toVariant2. Note that variant 2 UUIDs cannot be converted to variant 1 UUIDs.

Variant 2 UUIDs are very similar to variant 1 UUIDs, the main end-user difference being that they provide 1 fewer bit of randomness, but are a Microsoft standard. Note the single digit change in the following example:

var1UUID : UUID
var1UUID =
    fromString "12345678-1234-4321-bcde-123456789abc"
      |> Result.withDefault nil


canonical var1UUID
--> "12345678-1234-4321-bcde-123456789abc"


canonical (toVariant2 var1UUID)
-->"12345678-1234-4321-dcde-123456789abc"

Existing UUIDs

fromString : String -> Result String 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 "UUID was not correct length"


fromString "c72c207b-0847-386d-bdbc-2e5def81cg81"
--> Err "UUID contained non-hexadecimal digits"


fromString "00000000-0000-0000-0000-000000000000"
--> Ok nil


fromString "urn:uuid:00000000-0000-0000-0000-000000000000"
--> Ok nil


fromString "{00000000-0000-0000-0000-000000000000}"
--> Ok nil


fromString "\n\n     {urn:uuid: 00  000000-0000-0000-0-000-000000000000}"
--> Ok nil

Note: if you are decoding from JSON, you may like json-extra's fromResult function.

uuidDecoder = Json.Decode.string |> Json.Decode.andThen (Json.Decode.Extra.fromResult << UUID.fromString)

Formatting

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 Microsoft GUID representation and a URN representation.

toString : UUID -> String

This is just an alias for canonical, the most common way to represent a UUID.

import Random


someUUID : UUID
someUUID = Random.step generator (Random.initialSeed 345465) |> Tuple.first


toString someUUID
--> canonical someUUID
canonical : UUID -> String

Convert UUID to canonical textual representation

canonical nil
--> "00000000-0000-0000-0000-000000000000"
microsoftGUID : UUID -> String

Convert UUID to Microsoft GUID representation

microsoftGUID nil
--> "{00000000-0000-0000-0000-000000000000}"
urn : UUID -> String

Convert UUID to URN-namespaced representation

urn nil
--> "urn:uuid:00000000-0000-0000-0000-000000000000"

Inspecting UUIDs

Sometimes you may need to check a UUID's version, variant, or whether it's nil.

isNil : UUID -> Bool

True if the given UUID is "00000000-0000-0000-0000-000000000000".

version : UUID -> Maybe Int

If the bits of the UUID indicate that it is properly versioned UUID e.g. version 1, returns e.g. Just 1. Otherwise, Nothing!

version nil == Nothing


version (nil |> v3ChildNamed "Hello") == Just 3
isVersion : Int -> UUID -> Bool

True if the given UUID is the given version number. Always False is the integer provided is not 1, 2, 3, 4 or 5!

isVersion 8 someUUID -- always False!


isVersion 4 nil -- False!


someJsonValue
    |> Json.Decode.decodeValue UUID.decoder
    |> UUID.isVersion 5


isVersion 3 (nil |> v3ChildNamed "hello") -- True
variant : UUID -> Maybe Int

If the bits of the UUID indicate that it is a variant 1 or 2 UUID, returns Just 1 or Just 2, respectively. Otherwise, Nothing!

variant nil == Nothing


variant (nil |> v3ChildNamed "Hello") == Just 1
isVariant : Int -> UUID -> Bool

True if the given UUID is the given variant number. Always False is the integer provided is not 1 or 2!

isVariant 4 someUUID -- Always False, variant 4 doesn't exist!


someJsonValue
    |> Json.Decode.decodeValue UUID.decoder
    |> UUID.isVariant 1


isVariant 2 (someUUID |> toVariant2) -- True
isValid : UUID -> Bool

True if the given UUID correctly defines a version and variant. Note: the nil UUID is not valid!

isValid nil == False


Result.map UUID.isValid (UUID.fromString "c72c207b-0847-386d-bdbc-2e5def81cf81") == True


Result.map UUID.isValid (UUID.fromString "c72c207b-0847-986d-bdbc-2e5def81cf81") == False

Sometimes you may need to ensure that a UUID is definitely a certain version or variant. If you're decoding from JSON, you may find the following functions useful in conjunction with json-extra's fromResult function.

import Json.Decode import Json.Decode.Extra

uuidDecoder : Json.Decode.Decoder UUID
uuidDecoder =
    Json.Decode.string
        |> Json.Decode.andThen (Json.Decode.Extra.fromResult << UUID.fromString)
        |> Json.Decode.andThen (Json.Decode.Extra.fromResult << UUID.checkVersion 4)
        |> Json.Decode.andThen (Json.Decode.Extra.fromResult << UUID.checkVariant 1)


Json.Decode.decodeString uuidDecoder "\"00000000-0000-0000-0000-000000000000\""
    |> Result.toMaybe
--> Nothing


Json.Decode.decodeString uuidDecoder "\"7d25c9af-c80d-4304-a984-1d0b20fc581a\""
    |> Result.map UUID.canonical
--> Ok "7d25c9af-c80d-4304-a984-1d0b20fc581a"
checkNotNil : UUID -> Result String UUID

A simple function to use while chaining Results. Makes sure the UUID you're dealing with isn't the Nil UUID!

"8b681345-171d-48a0-9406-469d0a9de997"
    |> UUID.fromString
    |> Result.andThen checkNotNil
    |> Result.map canonical
--> Ok "8b681345-171d-48a0-9406-469d0a9de997"
checkVersion : Int -> UUID -> Result String UUID

Check which version a UUID is, and if it's not the right one, return an Err.

import Random


someVersion4UUID : UUID
someVersion4UUID = Random.step generator (Random.initialSeed 3476326) |> Tuple.first


someVersion4UUID |> checkVersion 4
--> Ok someVersion4UUID


nil |> v3ChildNamed "hello" |> checkVersion 4
--> Err "UUID is not Version 4"


nil |> checkVersion 1
--> Err "UUID does not define a valid version"
checkVariant : Int -> UUID -> Result String UUID

Check which variant a UUID is, and if it's not the right one, return an Err.

import Random


someVariant1UUID : UUID
someVariant1UUID = Random.step generator (Random.initialSeed 2355673) |> Tuple.first


someVariant1UUID |> checkVariant 1
--> Ok someVariant1UUID


nil |> v3ChildNamed "hello" |> checkVariant 2
--> Err "UUID is not variant 2"


nil |> checkVariant 2
--> Err "UUID does not define a valid variant"
checkValid : UUID -> Result String UUID

Checks if the UUID properly defines a version and variant, and if not return an Err. Note: the nil UUID is not valid!

checkValid nil == Err "UUID is nil"


UUID.fromString "c72c207b-0847-386d-bdbc-2e5def81cf81"
    |> Result.andThen UUID.checkValid
    |> Result.map UUID.canonical
    |> (==) Ok "c72c207b-0847-386d-bdbc-2e5def81cf81"


UUID.fromString "c72c207b-0847-986d-bdbc-2e5def81cf81"
    |> Result.andThen UUID.checkValid
    |> (==) Err "Version not defined"