Skip to content

Commit

Permalink
Mostly documentation update
Browse files Browse the repository at this point in the history
But also a light refactoring and a version bump
  • Loading branch information
vtomilin committed Oct 9, 2024
1 parent 3ef1d65 commit 6421a75
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 59 deletions.
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
[Universally Unique Lexicographically Sortable Identifier](https://github.com/ulid/spec) implementation in
Gleam.

What's a ULID? Some say it's a better UUID. In a string form it is shorter (26
characters vs. 32) and sortable.

# Caveats
1. Only Erlang build target is supported (at the moment)

Expand All @@ -16,14 +19,14 @@ gleam add gulid

```gleam
import gulid.{ new_as_string, new, new_monotonic, from_string_function,
to_string_function }
to_string_function }
pub fn main() {
// Quick and dirty ULID string - has performance implications
// Quick and dirty ULID string - performance implications
let ulid_str = new_as_string()
io.println("ULID is " <> ulid_str)
// Conver many ULIDs to String with a generator function
// Convert many ULIDs to String with a generator function
let to_string = to_string_function()
// `to_string_function` returns a function that then can be used to
// convert `Ulid` values to `String`. The reason this is done this way is
Expand Down Expand Up @@ -58,11 +61,11 @@ pub fn main() {
// Monotonic ULIDs:
// 1. Generate initial `Ulid` with `new()`
// 2. Then use `new_monotonic(Ulid)` with initial and subsequently generated
list.range(0, 9)
list.range(0, 9) // We want 10 monotonic ULIDs
|> list.scan(new(), fn(ulid, _) { new_monotonic(ulid) })
// Convert'em to strings
|> list.map(to_string_function())
|> io.debug
}
```
# Advanced Use
Expand Down Expand Up @@ -103,7 +106,7 @@ pub fn main() {
}
@external(erlang, "calendar", "system_time_to_rfc3339")
fn system_time_to_rfc3339(timestamp_millis_since_epoch: Int) -> List(Int)
fn system_time_to_rfc3339(seconds_since_epoch: Int) -> List(Int)
fn from_codepoints(code_points: List(Int)) -> String {
code_points
Expand All @@ -118,10 +121,16 @@ fn from_codepoints(code_points: List(Int)) -> String {

Further documentation can be found at <https://hexdocs.pm/gulid>.

## Development
## Examples

```sh
gleam run -m examples/example1 # Run the example one
gleam run -m examples/example2 # Run the example two
```

## Development

```sh
gleam test # Run the tests
```

8 changes: 2 additions & 6 deletions gleam.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
name = "gulid"
version = "0.0.1"
version = "0.0.2"

# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
description = "ULID implementation in Gleam (Erlang target only)"
licences = ["Apache-2.0"]
repository = { type = "github", user = "vtomilin", repo = "gulid" }
# links = [{ title = "Website", href = "" }]
internal_modules = [
"gulid/examples/*",
"examples/*",
]

[dependencies]
Expand Down
29 changes: 20 additions & 9 deletions src/examples/example1.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ import gleam/io
import gleam/list
import gulid.{
DecodeError, InvalidLength, from_string_function, new, new_as_string,
to_string_function,
new_monotonic, to_string_function,
}

pub fn main() {
// Quick and dirty ULID string - has performance implications
// Quick and dirty ULID string - performance implications
let ulid_str = new_as_string()
io.println("ULID is " <> ulid_str)

// Generate many ULIDs with a generator function
// Convert many ULIDs with a generator function
let to_string = to_string_function()
// `to_string_function` returns a function that then can be used to
// convert `Ulid` values to `String`. The reason this is done this way is
// that Gleam doesn't have a way to define module scoped global constants
// that could use function calls to initialize. Nor does it have module scoped
// `let`. Therefore, the only way to have a `private` reused value is to
// have it as a capture. So, there is a `let` in `to_string_function`, which
// binds an Erlang array with ULID character encodings, captured in returned
// function.
// that could allow function calls to initialize. Nor does it allow module
// scoped `let`. Therefore, the only way to have a `private` reused immutable
// value is to have it as a capture in a lamdba function. So, there is a
// `let` in `to_string_function`, which binds an Erlang array with ULID
// character encodings, captured in the returned function.
let bunch_of_ulids = list.map([new(), new(), new(), new(), new()], to_string)
io.println("A bunch of ULIDs:")
io.debug(bunch_of_ulids)
Expand All @@ -39,6 +39,17 @@ pub fn main() {
Nil
}
Error(InvalidLength(error)) | Error(DecodeError(error)) ->
io.println("Oh, noes: " <> error)
io.println("Oh, noes, the error: " <> error)
}

// Monotonic ULIDs:
io.println("Generating 10 monotonic ULIDs")
// 1. Generate initial `Ulid` with `new()`
// 2. Then use `new_monotonic(Ulid)` with initial and subsequently generated
list.range(0, 9)
// We want 10 monotonic ULIDs
|> list.scan(new(), fn(ulid, _) { new_monotonic(ulid) })
// Convert'em to strings
|> list.map(to_string_function())
|> io.debug
}
12 changes: 8 additions & 4 deletions src/examples/example2.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import gleam/io
import gleam/list
import gleam/result
import gleam/string
import gulid.{from_parts, from_tuple, to_parts}
import gulid.{from_parts, from_tuple, to_parts, to_string_function}

pub fn main() {
let to_string = to_string_function()
let ulid =
from_parts(
erlang.system_time(erlang.Millisecond),
int.random(99_999_999_999),
)
io.println("My ulid made from spare parts:")
io.println("My ulid made from spare parts: " <> to_string(ulid))
io.debug(ulid)

let #(timestamp, random) = to_parts(ulid)
Expand All @@ -22,16 +23,19 @@ pub fn main() {
"\tTime: "
<> { system_time_to_rfc3339(timestamp / 1000) |> from_codepoints },
)
// io.debug(system_time_to_rfc3339(timestamp / 1000))
io.println("\tRandom: " <> int.to_string(random))

let same_ulid = from_tuple(#(timestamp, random))
io.println(
"Now, we reconstruct a new ULID back from the earlier extracted components: "
<> to_string(same_ulid),
)

io.println("Same ulids? " <> { bool.to_string(same_ulid == ulid) })
}

@external(erlang, "calendar", "system_time_to_rfc3339")
fn system_time_to_rfc3339(timestamp_millis_since_epoch: Int) -> List(Int)
fn system_time_to_rfc3339(seconds_since_epoch: Int) -> List(Int)

fn from_codepoints(code_points: List(Int)) -> String {
code_points
Expand Down
70 changes: 37 additions & 33 deletions src/gulid.gleam
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
//// ## ULID Implementation in Gleam
////
//// ### Create
//// - `new()`: Non-monotonic.
//// - `new_monotonic(Ulid)`: Monotonic using previous `Ulid` value
//// - Parse ULID from a string:
//// ```gleam
//// let from_string = from_string_function()
//// from_string(new()) |> io.debug
//// ```
//// - `from_parts(Int, Int)`: Create from a timestamp milliseconds from Epoch
//// and random.
////
//// ### Convert to `String`
//// ```gleam
//// let to_string = to_string_function()
//// let ulid = new()
//// io.println("Ulid: " <> to_string(ulid))
//// ```

import gleam/bit_array
import gleam/crypto
import gleam/dict
Expand All @@ -8,31 +28,17 @@ import gleam/order
import gleam/result
import gleam/string

/// Represents an opaque `Ulid` type.
///
/// ## Create
/// - `new()`: Non-monotonic.
/// - `new_monotonic(Ulid)`: Monotonic using previous.
/// - `from_string(String)`: Parse ULID from a string.
/// - `from_parts(Int, Int)`: Create from a timestamp milliseconds from Epoch
/// and random.
///
/// ## Convert to `String`
/// ```gleam
/// let to_string = to_string_function()
/// let ulid = new()
/// io.println("Ulid: " <> to_string(ulid))
/// ```
/// Opaque `Ulid` type.
pub opaque type Ulid {
/// Create `Ulid` value from a raw `BitArray`
Ulid(BitArray)
}

/// Ulid module errors
/// Ulid module errors to be returned in a `Result`
pub type UlidError {
/// Returned when failed to decode
/// Returned when failed to decode `Ulid` from a `String`
DecodeError(mesage: String)
/// Returned when input is of incorrect length
/// Returned when input string is of incorrect length
InvalidLength(message: String)
}

Expand Down Expand Up @@ -67,14 +73,14 @@ pub fn to_string_function() -> fn(Ulid) -> String {
pub fn new() -> Ulid {
let time = erlang.system_time(erlang.Millisecond)
let randomness = crypto.strong_random_bytes(10)

Ulid(<<time:big-48, randomness:bits>>)
}

/// Returns new `Ulid` value based on given previous according to behavior,
/// described on ULID spec, basically, if previous has the same timestamp then
/// increment least significant bit of its random by 1 with carry to produce a
/// new `Ulid` (with the same timestamp).
/// Returns a new `Ulid` value based on given previous one according to the
/// behavior, described in the ULID spec, but basically, if the previous `Ulid`
/// has the same timestamp then increment the least significant bit of its
/// random value by 1 (with carry) to produce a new `Ulid` (with the same
/// timestamp).
pub fn new_monotonic(prev_ulid: Ulid) -> Ulid {
let time = erlang.system_time(erlang.Millisecond)
let assert Ulid(<<prev_time:unsigned-48, random:unsigned-80>>) = prev_ulid
Expand All @@ -84,8 +90,8 @@ pub fn new_monotonic(prev_ulid: Ulid) -> Ulid {
}
}

/// Returns a non-monotonic ULID value as string. Note, this is a shortcut,
/// not very good as far as the performance.
/// Returns a non-monotonic ULID value as a string. Note, this is a shortcut,
/// not very good for performance.
pub fn new_as_string() -> String {
new() |> to_string_function()
}
Expand Down Expand Up @@ -216,23 +222,21 @@ pub fn from_string_function() -> fn(String) -> Result(Ulid, UlidError) {
}
}

/// Returns an `Ulid` components `#(timestamp: Int, random: Int)` tuple.
/// Returns `Ulid` components in a `#(timestamp, random)` tuple.
pub fn to_parts(ulid: Ulid) -> #(Int, Int) {
// let assert <<timestamp:unsigned-48, random:unsigned-80>> =
// bit_array.append(ulid.timestamp, ulid.random)
let assert Ulid(<<timestamp:unsigned-48, random:unsigned-80>>) = ulid
#(timestamp, random)
}

/// Returns `Ulid` value, build from given integer timestamp (millis from Epoch)
/// and random values
pub fn from_parts(timestamp: Int, random: Int) -> Ulid {
/// Returns a `Ulid` value, build from a given integer timestamp (millis from
/// Epoch) and random values
pub fn from_parts(timestamp timestamp: Int, random random: Int) -> Ulid {
Ulid(<<timestamp:big-48, random:big-80>>)
}

/// Returns `Ulid` value, build from given (timestamp, random) tuple.
/// Returns a `Ulid` value, built from a given `#(timestamp, random)` tuple.
pub fn from_tuple(parts: #(Int, Int)) -> Ulid {
from_parts(parts.0, parts.1)
from_parts(timestamp: parts.0, random: parts.1)
}

//-- Private stuff
Expand Down

0 comments on commit 6421a75

Please sign in to comment.