├── .gitignore ├── README.md ├── gleam.toml ├── manifest.toml ├── src ├── carpenter.erl └── carpenter │ ├── internal │ └── ets_bindings.gleam │ └── table.gleam └── test └── carpenter_test.gleam /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | build 4 | erl_crash.dump 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # carpenter 🔨 2 | 3 | [![Package Version](https://img.shields.io/hexpm/v/carpenter)](https://hex.pm/packages/carpenter) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/carpenter/) 5 | 6 | Bindings for Erlang's [ETS tables](https://www.erlang.org/doc/man/ets.html). Forked and updated from [gts](https://github.com/Lunarmagpie/gts). 7 | 8 | If you aren't familiar with ETS tables, [this](https://elixirschool.com/en/lessons/storage/ets) is a good introduction. 9 | 10 | 11 | ## Quick start 12 | 13 | ```gleam 14 | import gleam/io 15 | import carpenter/table 16 | 17 | pub fn main() { 18 | // Set up and configure an ETS table 19 | let assert Ok(example) = 20 | table.build("table_name") 21 | |> table.privacy(table.Private) 22 | |> table.write_concurrency(table.AutoWriteConcurrency) 23 | |> table.read_concurrency(True) 24 | |> table.decentralized_counters(True) 25 | |> table.compression(False) 26 | |> table.set 27 | 28 | // Insert a value 29 | example 30 | |> table.insert([#("hello", "world")]) 31 | 32 | // Retrieve a key-value tuple 33 | example 34 | |> table.lookup("hello") 35 | |> io.debug 36 | 37 | // Delete an object 38 | example 39 | |> table.delete("hello") 40 | 41 | // Delete a table 42 | example 43 | |> table.drop 44 | } 45 | ``` 46 | 47 | ## Installation 48 | 49 | This package is available on hex: 50 | 51 | ```sh 52 | gleam add carpenter 53 | ``` 54 | 55 | Its documentation can be found at . 56 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "carpenter" 2 | version = "0.3.1" 3 | description = "Bindings for Erlang's ETS tables. Forked and updated from gts." 4 | target = "erlang" 5 | licences = ["MPL-2.0"] 6 | repository = { type = "github", user = "grottohub", repo = "carpenter" } 7 | links = [{ title = "Website", href = "https://github.com/grottohub/carpenter" }] 8 | 9 | [dependencies] 10 | gleam_stdlib = "~> 0.34 or ~> 1.0" 11 | gleam_erlang = "~> 0.24" 12 | 13 | [dev-dependencies] 14 | gleeunit = "~> 1.0" 15 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 6 | { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, 7 | { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 8 | ] 9 | 10 | [requirements] 11 | gleam_erlang = { version = "~> 0.24" } 12 | gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } 13 | gleeunit = { version = "~> 1.0" } 14 | -------------------------------------------------------------------------------- /src/carpenter.erl: -------------------------------------------------------------------------------- 1 | -module(carpenter). 2 | 3 | -export([new_table/2]). 4 | 5 | new_table(Name, Options) -> 6 | try 7 | {ok, ets:new(Name, Options)} 8 | catch 9 | error:badarg -> {error, nil} 10 | end. 11 | -------------------------------------------------------------------------------- /src/carpenter/internal/ets_bindings.gleam: -------------------------------------------------------------------------------- 1 | import gleam/dynamic 2 | import gleam/erlang/atom 3 | 4 | @external(erlang, "ets", "all") 5 | pub fn all() -> List(atom.Atom) 6 | 7 | @external(erlang, "ets", "delete") 8 | pub fn drop(table: atom.Atom) -> Nil 9 | 10 | @external(erlang, "ets", "delete") 11 | pub fn delete_key(table: atom.Atom, key: k) -> Nil 12 | 13 | @external(erlang, "ets", "delete_all_objects") 14 | pub fn delete_all_objects(table: atom.Atom) -> Nil 15 | 16 | @external(erlang, "ets", "delete_object") 17 | pub fn delete_object(table: atom.Atom, object: #(k, v)) -> Nil 18 | 19 | @external(erlang, "ets", "insert") 20 | pub fn insert(table: atom.Atom, tuple: List(#(k, v))) -> Nil 21 | 22 | @external(erlang, "ets", "insert_new") 23 | pub fn insert_new(table: atom.Atom, tuple: List(#(k, v))) -> Bool 24 | 25 | @external(erlang, "ets", "lookup") 26 | pub fn lookup(table: atom.Atom, key: k) -> List(#(k, v)) 27 | 28 | @external(erlang, "ets", "give_way") 29 | pub fn give_away(table: atom.Atom, pid: pid, gift_data: any) -> Nil 30 | 31 | @external(erlang, "carpenter", "new_table") 32 | pub fn new_table( 33 | name: atom.Atom, 34 | props: List(dynamic.Dynamic), 35 | ) -> Result(atom.Atom, Nil) 36 | 37 | @external(erlang, "ets", "member") 38 | pub fn member(table: atom.Atom, key: k) -> Bool 39 | 40 | @external(erlang, "ets", "take") 41 | pub fn take(table: atom.Atom, key: k) -> List(#(k, v)) 42 | -------------------------------------------------------------------------------- /src/carpenter/table.gleam: -------------------------------------------------------------------------------- 1 | import gleam/erlang/atom 2 | import gleam/erlang/process 3 | import gleam/dynamic 4 | import gleam/list 5 | import gleam/option.{type Option, None, Some} 6 | import carpenter/internal/ets_bindings 7 | 8 | pub type TableBuilder(k, v) { 9 | TableBuilder( 10 | name: String, 11 | privacy: Option(Privacy), 12 | write_concurrency: Option(WriteConcurrency), 13 | read_concurrency: Option(Bool), 14 | decentralized_counters: Option(Bool), 15 | compressed: Bool, 16 | ) 17 | } 18 | 19 | pub type Privacy { 20 | Private 21 | Protected 22 | Public 23 | } 24 | 25 | pub type WriteConcurrency { 26 | WriteConcurrency 27 | NoWriteConcurrency 28 | AutoWriteConcurrency 29 | } 30 | 31 | /// Begin building a new table with the given name. Ensure your table names are unique, 32 | /// otherwise you will encounter a `badarg` failure at runtime when attempting to build it. 33 | pub fn build(name: String) -> TableBuilder(k, v) { 34 | TableBuilder( 35 | name: name, 36 | privacy: None, 37 | write_concurrency: None, 38 | read_concurrency: None, 39 | decentralized_counters: None, 40 | compressed: False, 41 | ) 42 | } 43 | 44 | /// Set the privacy of the table. 45 | /// Acceptable values are `Private`, `Protected`, and `Public`. 46 | pub fn privacy( 47 | builder: TableBuilder(k, v), 48 | privacy: Privacy, 49 | ) -> TableBuilder(k, v) { 50 | TableBuilder(..builder, privacy: Some(privacy)) 51 | } 52 | 53 | /// Set the write_concurrency of the table. 54 | /// Acceptable values are `WriteConcurrency`, `NoWriteConcurrency`, or `AutoWriteConcurrency`. 55 | pub fn write_concurrency( 56 | builder: TableBuilder(k, v), 57 | con: WriteConcurrency, 58 | ) -> TableBuilder(k, v) { 59 | TableBuilder(..builder, write_concurrency: Some(con)) 60 | } 61 | 62 | /// Whether or not the table uses read_concurrency. Acceptable values are `True` or `False. 63 | pub fn read_concurrency( 64 | builder: TableBuilder(k, v), 65 | con: Bool, 66 | ) -> TableBuilder(k, v) { 67 | TableBuilder(..builder, read_concurrency: Some(con)) 68 | } 69 | 70 | /// Whether or not the table uses decentralized_counters. 71 | /// Acceptable values are `True` or `False`. 72 | /// 73 | /// You should probably choose `True` unless you are going to be polling 74 | /// the table for its size and memory usage frequently. 75 | pub fn decentralized_counters( 76 | builder: TableBuilder(k, v), 77 | counters: Bool, 78 | ) -> TableBuilder(k, v) { 79 | TableBuilder(..builder, decentralized_counters: Some(counters)) 80 | } 81 | 82 | /// Whether or not the table is compressed. 83 | pub fn compression( 84 | builder: TableBuilder(k, v), 85 | compressed: Bool, 86 | ) -> TableBuilder(k, v) { 87 | TableBuilder(..builder, compressed: compressed) 88 | } 89 | 90 | fn privacy_prop(prop: Privacy) -> dynamic.Dynamic { 91 | case prop { 92 | Private -> "private" 93 | Protected -> "protected" 94 | Public -> "public" 95 | } 96 | |> atom.create_from_string 97 | |> dynamic.from 98 | } 99 | 100 | fn write_concurrency_prop(prop: WriteConcurrency) -> dynamic.Dynamic { 101 | case prop { 102 | WriteConcurrency -> "true" 103 | NoWriteConcurrency -> "false" 104 | AutoWriteConcurrency -> "auto" 105 | } 106 | |> atom.create_from_string 107 | |> fn(x) { #(atom.create_from_string("write_concurrency"), x) } 108 | |> dynamic.from 109 | } 110 | 111 | fn build_table( 112 | builder: TableBuilder(k, v), 113 | table_type: String, 114 | ) -> Result(atom.Atom, Nil) { 115 | let name = atom.create_from_string(builder.name) 116 | 117 | let props = 118 | [ 119 | atom.create_from_string(table_type), 120 | atom.create_from_string("named_table"), 121 | ] 122 | |> list.map(dynamic.from) 123 | 124 | let props = case builder.privacy { 125 | Some(x) -> [privacy_prop(x), ..props] 126 | _ -> props 127 | } 128 | 129 | let props = case builder.write_concurrency { 130 | Some(x) -> [write_concurrency_prop(x), ..props] 131 | _ -> props 132 | } 133 | 134 | let props = case builder.read_concurrency { 135 | Some(x) -> [ 136 | #(atom.create_from_string("read_concurrency"), x) 137 | |> dynamic.from, 138 | ..props 139 | ] 140 | _ -> props 141 | } 142 | 143 | let props = case builder.compressed { 144 | True -> [ 145 | atom.create_from_string("compressed") 146 | |> dynamic.from, 147 | ..props 148 | ] 149 | False -> props 150 | } 151 | 152 | ets_bindings.new_table( 153 | name, 154 | props 155 | |> list.map(dynamic.from), 156 | ) 157 | } 158 | 159 | /// Specify table as a `set` 160 | pub fn set(builder: TableBuilder(k, v)) -> Result(Set(k, v), Nil) { 161 | case build_table(builder, "set") { 162 | Ok(t) -> Ok(Set(Table(t))) 163 | Error(_) -> Error(Nil) 164 | } 165 | } 166 | 167 | /// Specify table as an `ordered_set` 168 | pub fn ordered_set(builder: TableBuilder(k, v)) -> Result(Set(k, v), Nil) { 169 | case build_table(builder, "ordered_set") { 170 | Ok(t) -> Ok(Set(Table(t))) 171 | Error(_) -> Error(Nil) 172 | } 173 | } 174 | 175 | pub type Table(k, v) { 176 | Table(name: atom.Atom) 177 | } 178 | 179 | pub type Set(k, v) { 180 | Set(table: Table(k, v)) 181 | } 182 | 183 | /// Insert a list of objects into the set 184 | pub fn insert(set: Set(k, v), objects: List(#(k, v))) -> Nil { 185 | ets_bindings.insert(set.table.name, objects) 186 | } 187 | 188 | /// Insert a list of objects without overwriting any existing keys. 189 | /// 190 | /// This will not insert ANY object unless ALL keys do not exist. 191 | pub fn insert_new(set: Set(k, v), objects: List(#(k, v))) -> Bool { 192 | ets_bindings.insert_new(set.table.name, objects) 193 | } 194 | 195 | /// Retrieve a list of objects from the table. 196 | pub fn lookup(set: Set(k, v), key: k) -> List(#(k, v)) { 197 | ets_bindings.lookup(set.table.name, key) 198 | } 199 | 200 | /// Delete all objects with key `key` from the table. 201 | pub fn delete(set: Set(k, v), key: k) -> Nil { 202 | ets_bindings.delete_key(set.table.name, key) 203 | } 204 | 205 | /// Delete all objects belonging to a table 206 | pub fn delete_all(set: Set(k, v)) -> Nil { 207 | ets_bindings.delete_all_objects(set.table.name) 208 | } 209 | 210 | /// Delete an exact object from the table 211 | pub fn delete_object(set: Set(k, v), object: #(k, v)) -> Nil { 212 | ets_bindings.delete_object(set.table.name, object) 213 | } 214 | 215 | /// Deletes the entire table. 216 | pub fn drop(set: Set(k, v)) { 217 | ets_bindings.drop(set.table.name) 218 | } 219 | 220 | /// Give the table to another process. 221 | pub fn give_away(set: Set(k, v), pid: process.Pid, gift_data: any) -> Nil { 222 | ets_bindings.give_away(set.table.name, pid, gift_data) 223 | } 224 | 225 | /// Returns a boolean based on the existence of a key within the table 226 | pub fn contains(set: Set(k, v), key: k) -> Bool { 227 | ets_bindings.member(set.table.name, key) 228 | } 229 | 230 | /// Get a reference to an existing table 231 | pub fn ref(name: String) -> Result(Set(k, v), Nil) { 232 | case atom.from_string(name) { 233 | Ok(t) -> Ok(Set(Table(t))) 234 | Error(_) -> Error(Nil) 235 | } 236 | } 237 | 238 | /// Return and remove a list of objects with the given key 239 | pub fn take(set: Set(k, v), key: k) -> List(#(k, v)) { 240 | ets_bindings.take(set.table.name, key) 241 | } 242 | -------------------------------------------------------------------------------- /test/carpenter_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit 2 | import gleeunit/should 3 | import carpenter/table 4 | 5 | pub fn main() { 6 | gleeunit.main() 7 | } 8 | 9 | pub fn set_insert_test() { 10 | let t = 11 | table.build("set_insert_test") 12 | |> table.set 13 | |> should.be_ok 14 | 15 | t 16 | |> table.insert([#("hello", "world")]) 17 | t 18 | |> table.lookup("hello") 19 | |> should.equal([#("hello", "world")]) 20 | } 21 | 22 | pub fn set_insert_new_test() { 23 | let t = 24 | table.build("set_insert_new_test") 25 | |> table.set 26 | |> should.be_ok 27 | 28 | t 29 | |> table.insert([#(1, 2), #(2, 3)]) 30 | t 31 | |> table.insert_new([#(3, 4), #(1, 3)]) 32 | |> should.be_false 33 | t 34 | |> table.insert_new([#(3, 4), #(4, 5)]) 35 | |> should.be_true 36 | } 37 | 38 | pub fn set_delete_test() { 39 | let t = 40 | table.build("delete_test") 41 | |> table.set 42 | |> should.be_ok 43 | 44 | t 45 | |> table.insert([#(1, 2)]) 46 | t 47 | |> table.delete(1) 48 | t 49 | |> table.lookup(1) 50 | |> should.equal([]) 51 | } 52 | 53 | pub fn set_delete_all_test() { 54 | let t = 55 | table.build("delete_all_test") 56 | |> table.set 57 | |> should.be_ok 58 | 59 | t 60 | |> table.insert([#(1, 2), #(2, 3)]) 61 | t 62 | |> table.delete_all 63 | 64 | t 65 | |> table.lookup(1) 66 | |> should.equal([]) 67 | 68 | t 69 | |> table.lookup(2) 70 | |> should.equal([]) 71 | } 72 | 73 | pub fn set_delete_object_test() { 74 | let t = 75 | table.build("delete_obj_test") 76 | |> table.set 77 | |> should.be_ok 78 | 79 | t 80 | |> table.insert([#(1, 2)]) 81 | t 82 | |> table.delete_object(#(1, 2)) 83 | t 84 | |> table.contains(1) 85 | |> should.be_false 86 | } 87 | 88 | pub fn ordered_set_test() { 89 | let t = 90 | table.build("ordered_set_test") 91 | |> table.ordered_set 92 | |> should.be_ok 93 | 94 | t 95 | |> table.insert([#(1, 2), #(2, 3)]) 96 | t 97 | |> table.lookup(1) 98 | |> should.equal([#(1, 2)]) 99 | } 100 | 101 | pub fn drop_test() { 102 | let t = 103 | table.build("drop_test") 104 | |> table.set 105 | |> should.be_ok 106 | 107 | table.build("drop_test") 108 | |> table.set 109 | |> should.be_error 110 | 111 | t 112 | |> table.drop 113 | 114 | table.build("drop_test") 115 | |> table.set 116 | |> should.be_ok 117 | } 118 | 119 | pub fn contains_test() { 120 | let t = 121 | table.build("contains_test") 122 | |> table.set 123 | |> should.be_ok 124 | 125 | t 126 | |> table.insert([#(1, 2)]) 127 | 128 | t 129 | |> table.contains(1) 130 | |> should.be_true 131 | t 132 | |> table.contains(2) 133 | |> should.be_false 134 | } 135 | 136 | pub fn ref_test() { 137 | let t = 138 | table.ref("contains_test") 139 | |> should.be_ok 140 | 141 | t 142 | |> table.contains(1) 143 | |> should.be_true 144 | } 145 | 146 | pub fn take_test() { 147 | let t = 148 | table.build("take_test") 149 | |> table.set 150 | |> should.be_ok 151 | 152 | t 153 | |> table.insert([#(1, 2)]) 154 | t 155 | |> table.contains(1) 156 | |> should.be_true 157 | t 158 | |> table.take(1) 159 | |> should.equal([#(1, 2)]) 160 | t 161 | |> table.contains(1) 162 | |> should.be_false 163 | } 164 | --------------------------------------------------------------------------------