//// An outrageously simple set of functions to interact with the BEAM //// DETS (Disk-based Erlang Term Storage) API. //// //// Good for small projects and POCs. //// //// //// This project DOES NOT intend to serve as direct bindings to the //// DETS API, but rather to interact with it in a gleamy way: //// 1. with a simple and concise interface; //// 2. type-safely; //// 3. no unexpected crashes, all errors are values. import gleam/erlang/atom.{type Atom} import gleam/erlang/charlist // BEAM interaction type Storage { Set } type TableAttributes { File(charlist.Charlist) Type(Storage) Keypos(Int) } /// A reference to an open table, required to interact with said table. /// Obtained through the `transaction(Table)` function pub type TableRef(a) @external(erlang, "dets", "open_file") fn dets_open_file( name: Atom, att: List(TableAttributes), ) -> Result(TableRef(a), reason) @external(erlang, "dets", "close") fn dets_close(tab: TableRef(a)) -> Result(b, c) @external(erlang, "dets", "insert") fn dets_insert(tab: TableRef(a), value: a) -> Result(b, c) @external(erlang, "dets", "delete") fn dets_delete(tab: TableRef(a), index: b) -> Result(b, c) @external(erlang, "dets", "lookup") fn dets_lookup(tab: TableRef(a), index: b) -> List(a) @external(erlang, "file", "delete") fn file_delete(path: charlist.Charlist) -> a @external(erlang, "erlang", "element") fn erlang_element(index: Int, tuple: a) -> Atom @external(erlang, "erlang", "is_tuple") fn erlang_is_tuple(tuple: a) -> Bool @external(erlang, "erlang", "is_atom") fn erlang_is_atom(atom: Atom) -> Bool @external(erlang, "erlang", "tuple_size") fn erlang_tuple_size(tuple: a) -> Int // Type-safe API /// A collection of values used to access a DETS table. pub opaque type Table(a) { Table( tabname: Atom, attributes: List(TableAttributes), path: charlist.Charlist, ) } /// Possible error that may occur when using the library. pub type TableError { /// The sample provided is not a Gleam record or /// the index provided is not present within said record. Badarg /// A problem occurred when trying to open and lock /// the .dets file. UnableToOpen /// A problem ocurred when trying to write into the /// .dets file and close it. UnableToClose } /// Creats a table. /// /// If no .dets file exists for the provided sample, creates one. /// Otherwise, just checks whether the file is accessible and not /// corrupted. /// /// # Example /// /// ```gleam /// pub fn start_database() { /// let pluto = Pet(name: "Pluto", animal: Dog) /// database.create_table(sample: pluto, index_at: 0) /// // -> Ok(Table(Pet)) /// } /// ``` /// pub fn create_table( sample sample: a, index_at keypos: Int, ) -> Result(Table(a), TableError) { case is_record(sample), keypos >= 0 { True, True -> case keypos + 2 > erlang_tuple_size(sample) { True -> Error(Badarg) False -> { let at = erlang_element(1, sample) let name = atom.to_string(at) let path = charlist.from_string(name <> ".dets") let att = [File(path), Type(Set), Keypos(keypos + 2)] // +2 because Erlang arrays start at 1 and the first value from our tuple will always be its atom case dets_open_file(at, att) { Ok(tab) -> { case dets_close(tab) { Error(_) -> Error(UnableToClose) _ -> Ok(Table(at, att, path)) } } Error(_) -> Error(UnableToOpen) } } } _, _ -> Error(Badarg) } } /// Allows you to interact with the table. /// /// It opens and locks the .dets file, then execute your operations. /// Once the operations are done, it writes the changes into the file, /// closes and releases it. /// /// # Example /// /// ```gleam /// pub fn is_pet_registered(table: Table(Pet), petname: String) { /// use ref <- database.transaction(table) /// case database.find(ref, petname) { /// Ok(_) -> True /// Error(Nil) -> False /// } /// } /// ``` /// pub fn transaction( table: Table(a), procedure: fn(TableRef(a)) -> b, ) -> Result(b, TableError) { case dets_open_file(table.tabname, table.attributes) { Error(_) -> Error(UnableToOpen) Ok(ref) -> { let resp = procedure(ref) case dets_close(ref) { Error(_) -> Error(UnableToClose) _ -> Ok(resp) } } } } /// Inserts a value into a table. /// /// DETS tables do not have support for update, only for upsert. /// So if you have to change a value, just insert a new value /// with the same index, and it will replace the previous value. /// /// # Example /// /// ```gleam /// pub fn new_pet(table: Table(Pet), animal: Animal, name: String) { /// let pet = Pet(name, animal) /// let op = database.transaction(table, fn(ref) { /// database.insert(ref, pet) /// }) /// case op { /// Ok(_) -> Ok(pet) /// Error(reason) -> Error(reason) /// } /// } /// ``` /// pub fn insert(transac: TableRef(a), value: a) { case dets_insert(transac, value) { Error(reason) -> Error(reason) _ -> Ok(Nil) } } /// Deletes a value from a table. /// /// # Example /// /// ```gleam /// pub fn delete_pet(table: Table(Pet) petname: String) { /// use ref <- database.transaction(table) /// database.delete(ref, petname) /// } /// ``` /// pub fn delete(transac: TableRef(a), index: b) { case dets_delete(transac, index) { Error(reason) -> Error(reason) _ -> Ok(Nil) } } /// Finds a value by its index /// /// # Example /// /// ```gleam /// pub fn play_with_pluto(table: Table(Pet)) { /// use ref <- database.transaction(table) /// let resp = database.find(ref, "Pluto") /// case resp { /// Error(_) -> Error(PlutoNotFoundBlameTheAstronomers) /// Ok(pluto) -> Ok(play_with(pluto)) /// } /// } /// ``` /// pub fn find(transac: TableRef(a), index: b) -> Result(a, Nil) { case dets_lookup(transac, index) { [resp] -> Ok(resp) _ -> Error(Nil) } } /// Deletes de entire table file /// /// # Example /// /// ```gleam /// pub fn destroy_all_pets(table: Table(Pet), password: String) { /// case password { /// "Yes, I am evil." -> { /// database.drop(table) /// Ok(Nil) /// } /// _ -> Error(WrongPassword) /// } /// } /// ``` /// pub fn drop_table(table: Table(a)) { case file_delete(table.path) { Error(reason) -> Error(reason) _ -> Ok(Nil) } } fn is_record(value: a) { erlang_is_tuple(value) && erlang_is_atom(erlang_element(1, value)) }