Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mostly a documentation update #4

Merged
merged 1 commit into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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