diff --git a/README.md b/README.md index e8e9438..531bb67 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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 @@ -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 @@ -118,10 +121,16 @@ fn from_codepoints(code_points: List(Int)) -> String { Further documentation can be found at . -## 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 ``` + diff --git a/gleam.toml b/gleam.toml index ea79584..cc8be15 100644 --- a/gleam.toml +++ b/gleam.toml @@ -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] diff --git a/src/examples/example1.gleam b/src/examples/example1.gleam index 254c1ef..e48716f 100644 --- a/src/examples/example1.gleam +++ b/src/examples/example1.gleam @@ -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) @@ -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 } diff --git a/src/examples/example2.gleam b/src/examples/example2.gleam index c30a223..2c1e0f1 100644 --- a/src/examples/example2.gleam +++ b/src/examples/example2.gleam @@ -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) @@ -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 diff --git a/src/gulid.gleam b/src/gulid.gleam index 3b0c29d..6539010 100644 --- a/src/gulid.gleam +++ b/src/gulid.gleam @@ -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 @@ -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) } @@ -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(<>) } -/// 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_ulid @@ -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() } @@ -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 <> = - // bit_array.append(ulid.timestamp, ulid.random) let assert Ulid(<>) = 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(<>) } -/// 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