-module(database). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -define(FILEPATH, "src\\database.gleam"). -export([transaction/2, insert/2, delete/2, find/2, drop_table/1, create_table/2]). -export_type([storage/0, table_attributes/0, table_ref/1, table/1, table_error/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " An outrageously simple set of functions to interact with the BEAM\n" " DETS (Disk-based Erlang Term Storage) API.\n" " \n" " Good for small projects and POCs.\n" "\n" "\n" " This project DOES NOT intend to serve as direct bindings to the \n" " DETS API, but rather to interact with it in a gleamy way:\n" " 1. with a simple and concise interface;\n" " 2. type-safely;\n" " 3. no unexpected crashes, all errors are values.\n" ). -type storage() :: set. -type table_attributes() :: {file, gleam@erlang@charlist:charlist()} | {type, storage()} | {keypos, integer()}. -type table_ref(EIC) :: any() | {gleam_phantom, EIC}. -opaque table(EID) :: {table, gleam@erlang@atom:atom_(), list(table_attributes()), gleam@erlang@charlist:charlist()} | {gleam_phantom, EID}. -type table_error() :: badarg | unable_to_open | unable_to_close. -file("src\\database.gleam", 156). ?DOC( " Allows you to interact with the table.\n" "\n" " It opens and locks the .dets file, then execute your operations.\n" " Once the operations are done, it writes the changes into the file,\n" " closes and releases it.\n" "\n" " # Example\n" "\n" " ```gleam\n" " pub fn is_pet_registered(table: Table(Pet), petname: String) {\n" " use ref <- database.transaction(table)\n" " case database.find(ref, petname) {\n" " Ok(_) -> True\n" " Error(Nil) -> False\n" " }\n" " }\n" " ```\n" ). -spec transaction(table(EJO), fun((table_ref(EJO)) -> EJR)) -> {ok, EJR} | {error, table_error()}. transaction(Table, Procedure) -> case dets:open_file(erlang:element(2, Table), erlang:element(3, Table)) of {error, _} -> {error, unable_to_open}; {ok, Ref} -> Resp = Procedure(Ref), case dets:close(Ref) of {error, _} -> {error, unable_to_close}; _ -> {ok, Resp} end end. -file("src\\database.gleam", 193). ?DOC( " Inserts a value into a table.\n" "\n" " DETS tables do not have support for update, only for upsert.\n" " So if you have to change a value, just insert a new value\n" " with the same index, and it will replace the previous value.\n" "\n" " # Example\n" "\n" " ```gleam\n" " pub fn new_pet(table: Table(Pet), animal: Animal, name: String) {\n" " let pet = Pet(name, animal)\n" " let op = database.transaction(table, fn(ref) {\n" " database.insert(ref, pet)\n" " })\n" " case op {\n" " Ok(_) -> Ok(pet)\n" " Error(reason) -> Error(reason)\n" " }\n" " }\n" " ```\n" ). -spec insert(table_ref(EJU), EJU) -> {ok, nil} | {error, any()}. insert(Transac, Value) -> case dets:insert(Transac, Value) of {error, Reason} -> {error, Reason}; _ -> {ok, nil} end. -file("src\\database.gleam", 211). ?DOC( " Deletes a value from a table.\n" "\n" " # Example\n" "\n" " ```gleam\n" " pub fn delete_pet(table: Table(Pet) petname: String) {\n" " use ref <- database.transaction(table)\n" " database.delete(ref, petname)\n" " }\n" " ```\n" ). -spec delete(table_ref(any()), any()) -> {ok, nil} | {error, any()}. delete(Transac, Index) -> case dets:delete(Transac, Index) of {error, Reason} -> {error, Reason}; _ -> {ok, nil} end. -file("src\\database.gleam", 233). ?DOC( " Finds a value by its index\n" "\n" " # Example\n" "\n" " ```gleam\n" " pub fn play_with_pluto(table: Table(Pet)) {\n" " use ref <- database.transaction(table)\n" " let resp = database.find(ref, \"Pluto\")\n" " case resp {\n" " Error(_) -> Error(PlutoNotFoundBlameTheAstronomers)\n" " Ok(pluto) -> Ok(play_with(pluto))\n" " }\n" " }\n" " ```\n" ). -spec find(table_ref(EKB), any()) -> {ok, EKB} | {error, nil}. find(Transac, Index) -> case dets:lookup(Transac, Index) of [Resp] -> {ok, Resp}; _ -> {error, nil} end. -file("src\\database.gleam", 256). ?DOC( " Deletes de entire table file\n" "\n" " # Example\n" "\n" " ```gleam\n" " pub fn destroy_all_pets(table: Table(Pet), password: String) {\n" " case password {\n" " \"Yes, I am evil.\" -> {\n" " database.drop(table)\n" " Ok(Nil)\n" " }\n" " _ -> Error(WrongPassword)\n" " }\n" " }\n" " ```\n" ). -spec drop_table(table(any())) -> {ok, nil} | {error, any()}. drop_table(Table) -> case file:delete(erlang:element(4, Table)) of {error, Reason} -> {error, Reason}; _ -> {ok, nil} end. -file("src\\database.gleam", 263). -spec is_record(any()) -> boolean(). is_record(Value) -> erlang:is_tuple(Value) andalso erlang:is_atom(erlang:element(1, Value)). -file("src\\database.gleam", 107). ?DOC( " Creats a table.\n" "\n" " If no .dets file exists for the provided sample, creates one.\n" " Otherwise, just checks whether the file is accessible and not\n" " corrupted.\n" "\n" " # Example\n" "\n" " ```gleam\n" " pub fn start_database() {\n" " let pluto = Pet(name: \"Pluto\", animal: Dog)\n" " database.create_table(sample: pluto, index_at: 0)\n" " // -> Ok(Table(Pet))\n" " }\n" " ```\n" ). -spec create_table(EJK, integer()) -> {ok, table(EJK)} | {error, table_error()}. create_table(Sample, Keypos) -> case {is_record(Sample), Keypos >= 0} of {true, true} -> case (Keypos + 2) > erlang:tuple_size(Sample) of true -> {error, badarg}; false -> At = erlang:element(1, Sample), Name = erlang:atom_to_binary(At), Path = unicode:characters_to_list( <> ), Att = [{file, Path}, {type, set}, {keypos, Keypos + 2}], case dets:open_file(At, Att) of {ok, Tab} -> case dets:close(Tab) of {error, _} -> {error, unable_to_close}; _ -> {ok, {table, At, Att, Path}} end; {error, _} -> {error, unable_to_open} end end; {_, _} -> {error, badarg} end.