• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

satoren / y_ex / 480f32fa342175ef165d4a5d92c6309b0bfa9904-PR-198

21 Nov 2025 02:03AM UTC coverage: 95.447% (-1.1%) from 96.534%
480f32fa342175ef165d4a5d92c6309b0bfa9904-PR-198

Pull #198

github

satoren
fix
Pull Request #198: Add *_and_get functions that return values directly

43 of 52 new or added lines in 5 files covered. (82.69%)

1 existing line in 1 file now uncovered.

587 of 615 relevant lines covered (95.45%)

22.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.65
/lib/shared_type/map.ex
1
defmodule Yex.Map do
2
  @moduledoc """
3
  A shareable Map type that supports concurrent modifications with automatic conflict resolution.
4
  This module provides functionality for collaborative key-value pair management with support for
5
  nested shared types and JSON compatibility.
6

7
  ## Features
8
  - Concurrent modifications with automatic conflict resolution
9
  - Support for nested shared types (Array, Text, Map)
10
  - JSON-compatible serialization
11
  - Key-value pair management with atomic operations
12
  - Observable changes for real-time collaboration
13
  """
14

15
  defstruct [
16
    :doc,
17
    :reference
18
  ]
19

20
  @type t :: %__MODULE__{
21
          doc: Yex.Doc.t(),
22
          reference: reference()
23
        }
24

25
  alias Yex.Doc
26
  require Yex.Doc
27

28
  @type value :: term()
29

30
  @doc """
31
  Sets a key-value pair in the map.
32
  Returns :ok on success, :error on failure.
33

34
  ## Parameters
35
    * `map` - The map to modify
36
    * `key` - The key to set
37
    * `content` - The value to associate with the key
38

39
  ## Examples
40
      iex> doc = Yex.Doc.new()
41
      iex> map = Yex.Doc.get_map(doc, "map")
42
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
43
      :ok
44
  """
45
  @spec set(t, binary(), Yex.input_type()) :: :ok
46
  def set(%__MODULE__{doc: doc} = map, key, content) when is_binary(key) do
47
    Doc.run_in_worker_process(doc,
53✔
48
      do: Yex.Nif.map_set(map, cur_txn(map), key, content)
53✔
49
    )
50
  end
51

52
  @doc """
53
  Sets a key-value pair in the map and returns the set value.
54
  Returns the value on success, raises on failure.
55

56
  ## Parameters
57
    * `map` - The map to modify
58
    * `key` - The key to set
59
    * `content` - The value to associate with the key
60

61
  ## Examples
62
      iex> doc = Yex.Doc.new()
63
      iex> map = Yex.Doc.get_map(doc, "map")
64
      iex> value = Yex.Map.set_and_get(map, "plane", ["Hello", "World"])
65
      iex> value
66
      ["Hello", "World"]
67
  """
68
  @spec set_and_get(t, binary(), Yex.input_type()) :: value()
69
  def set_and_get(%__MODULE__{doc: doc} = map, key, content) when is_binary(key) do
70
    Doc.run_in_worker_process doc do
4✔
71
      :ok = Yex.Nif.map_set(map, cur_txn(map), key, content)
4✔
72

73
      case Yex.Nif.map_get(map, cur_txn(map), key) do
4✔
74
        {:ok, value} -> value
4✔
NEW
75
        :error -> raise RuntimeError, "Failed to get inserted value"
×
76
      end
77
    end
78
  end
79

80
  @doc """
81
  Deletes a key from the map.
82
  Returns :ok on success, :error on failure.
83

84
  ## Parameters
85
    * `map` - The map to modify
86
    * `key` - The key to delete
87

88
  ## Examples
89
      iex> doc = Yex.Doc.new()
90
      iex> map = Yex.Doc.get_map(doc, "map")
91
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
92
      iex> Yex.Map.delete(map, "plane")
93
      :ok
94
  """
95
  @spec delete(t, binary()) :: :ok
96
  def delete(%__MODULE__{doc: doc} = map, key) when is_binary(key) do
97
    Doc.run_in_worker_process(doc,
2✔
98
      do: Yex.Nif.map_delete(map, cur_txn(map), key)
2✔
99
    )
100
  end
101

102
  @doc """
103
  get a key from the map.
104
    ## Examples
105
      iex> doc = Yex.Doc.new()
106
      iex> map = Yex.Doc.get_map(doc, "map")
107
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
108
      iex> Yex.Map.get(map, "plane")
109
      ["Hello", "World"]
110
      iex> Yex.Map.get(map, "not_found")
111
      nil
112
  """
113
  @spec get(t, binary(), default :: value()) :: value()
114
  def get(%__MODULE__{} = map, key, default \\ nil) do
115
    case fetch(map, key) do
3✔
116
      {:ok, value} -> value
1✔
117
      :error -> default
2✔
118
    end
119
  end
120

121
  @doc """
122
  Gets a value by key from the map, or lazily evaluates the given function if the key is not found.
123
  This is useful when the default value is expensive to compute and should only be evaluated when needed.
124

125
  ## Parameters
126
    * `map` - The map to query
127
    * `key` - The key to look up
128
    * `fun` - A function that returns the default value (only called if key is not found)
129

130
  ## Examples
131
      iex> doc = Yex.Doc.new()
132
      iex> map = Yex.Doc.get_map(doc, "map")
133
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
134
      iex> Yex.Map.get_lazy(map, "plane", fn -> ["Default"] end)
135
      ["Hello", "World"]
136
      iex> Yex.Map.get_lazy(map, "not_found", fn -> ["Computed"] end)
137
      ["Computed"]
138

139
  Particularly useful with `set_and_get/3` for get-or-create patterns:
140

141
      iex> doc = Yex.Doc.new()
142
      iex> map = Yex.Doc.get_map(doc, "map")
143
      iex> # Get existing value or create and return new one
144
      iex> value = Yex.Map.get_lazy(map, "counter", fn ->
145
      ...>   Yex.Map.set_and_get(map, "counter", 0)
146
      ...> end)
147
      iex> value
148
      0
149
      iex> # Next call returns existing value without calling the function
150
      iex> Yex.Map.get_lazy(map, "counter", fn -> Yex.Map.set_and_get(map, "counter", 0) end)
151
      0
152
  """
153
  @spec get_lazy(t, binary(), fun :: (-> value())) :: value()
154
  def get_lazy(%__MODULE__{} = map, key, fun) do
155
    case fetch(map, key) do
2✔
156
      {:ok, value} -> value
1✔
157
      :error -> fun.()
1✔
158
    end
159
  end
160

161
  @doc """
162
  Retrieves a value by key from the map.
163
  Returns {:ok, value} if found, :error if not found.
164

165
  ## Parameters
166
    * `map` - The map to query
167
    * `key` - The key to look up
168

169
  ## Examples
170
      iex> doc = Yex.Doc.new()
171
      iex> map = Yex.Doc.get_map(doc, "map")
172
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
173
      iex> Yex.Map.fetch(map, "plane")
174
      {:ok, ["Hello", "World"]}
175
      iex> Yex.Map.fetch(map, "not_found")
176
      :error
177
  """
178
  @spec fetch(t, binary()) :: {:ok, value()} | :error
179
  def fetch(%__MODULE__{doc: doc} = map, key) when is_binary(key) do
180
    Doc.run_in_worker_process(doc,
36✔
181
      do: Yex.Nif.map_get(map, cur_txn(map), key)
36✔
182
    )
183
  end
184

185
  @doc """
186
  Similar to fetch/2 but raises ArgumentError if the key is not found.
187

188
  ## Parameters
189
    * `map` - The map to query
190
    * `key` - The key to look up
191

192
  ## Raises
193
    * ArgumentError - If the key is not found
194
  """
195
  @spec fetch!(t, binary()) :: value()
196
  def fetch!(%__MODULE__{} = map, key) when is_binary(key) do
197
    case fetch(map, key) do
5✔
198
      {:ok, value} -> value
3✔
199
      :error -> raise ArgumentError, "Key not found"
2✔
200
    end
201
  end
202

203
  @doc """
204
  Checks if a key exists in the map.
205
  Returns true if the key exists, false otherwise.
206

207
  ## Parameters
208
    * `map` - The map to check
209
    * `key` - The key to look for
210
  """
211
  @spec has_key?(t, binary()) :: boolean()
212
  def has_key?(%__MODULE__{doc: doc} = map, key) when is_binary(key) do
213
    Doc.run_in_worker_process(doc,
2✔
214
      do: Yex.Nif.map_contains_key(map, cur_txn(map), key)
2✔
215
    )
216
  end
217

218
  @doc """
219
  Converts the map to a standard Elixir map.
220
  This is useful when you need to work with the map's contents in a non-collaborative context.
221

222
  ## Examples
223
      iex> doc = Yex.Doc.new()
224
      iex> map = Yex.Doc.get_map(doc, "map")
225
      iex> Yex.Map.set(map, "array", Yex.ArrayPrelim.from(["Hello", "World"]))
226
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
227
      iex> assert %{"plane" => ["Hello", "World"], "array" => %Yex.Array{}} = Yex.Map.to_map(map)
228
  """
229
  @spec to_map(t) :: map()
230
  def to_map(%__MODULE__{doc: doc} = map) do
231
    Doc.run_in_worker_process(doc,
17✔
232
      do: Yex.Nif.map_to_map(map, cur_txn(map))
17✔
233
    )
234
  end
235

236
  @doc """
237
  Converts the map to a list of key-value tuples.
238
  ## Examples
239
      iex> doc = Yex.Doc.new()
240
      iex> map = Yex.Doc.get_map(doc, "map")
241
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
242
      iex> Yex.Map.to_list(map)
243
      [{"plane", ["Hello", "World"]}]
244
  """
245
  @spec to_list(t) :: list({binary(), term()})
246
  def to_list(map) do
247
    to_map(map) |> Enum.to_list()
4✔
248
  end
249

250
  @doc """
251
  Returns the number of key-value pairs in the map.
252
  """
253
  @spec size(t) :: integer()
254
  def size(%__MODULE__{doc: doc} = map) do
255
    Doc.run_in_worker_process(doc,
4✔
256
      do: Yex.Nif.map_size(map, cur_txn(map))
4✔
257
    )
258
  end
259

260
  @doc """
261
  Converts the map to a JSON-compatible format.
262
  This is useful for serialization or when you need to transfer the map's contents.
263

264
  ## Examples
265
      iex> doc = Yex.Doc.new()
266
      iex> map = Yex.Doc.get_map(doc, "map")
267
      iex> Yex.Map.set(map, "array", Yex.ArrayPrelim.from(["Hello", "World"]))
268
      iex> Yex.Map.set(map, "plane", ["Hello", "World"])
269
      iex> assert %{"plane" => ["Hello", "World"], "array" => ["Hello", "World"]} = Yex.Map.to_json(map)
270
  """
271
  @spec to_json(t) :: map()
272
  def to_json(%__MODULE__{doc: doc} = map) do
273
    Doc.run_in_worker_process(doc,
1✔
274
      do: Yex.Nif.map_to_json(map, cur_txn(map))
1✔
275
    )
276
  end
277

278
  @doc false
279
  # Gets the current transaction reference from the process dictionary for the given document
280
  defp cur_txn(%{doc: %Yex.Doc{reference: doc_ref}}) do
281
    Process.get(doc_ref, nil)
123✔
282
  end
283

284
  @doc """
285
  Converts the map to its preliminary representation.
286
  This is useful when you need to serialize or transfer the map's contents.
287

288
  ## Parameters
289
    * `map` - The map to convert
290
  """
291
  @spec as_prelim(t) :: Yex.MapPrelim.t()
292
  def as_prelim(%__MODULE__{doc: doc} = map) do
293
    Doc.run_in_worker_process(doc,
1✔
294
      do:
295
        Yex.Map.to_list(map)
296
        |> Enum.map(fn {key, value} -> {key, Yex.Output.as_prelim(value)} end)
2✔
297
        |> Map.new()
298
        |> Yex.MapPrelim.from()
1✔
299
    )
300
  end
301

302
  defimpl Yex.Output do
303
    def as_prelim(map) do
304
      Yex.Map.as_prelim(map)
×
305
    end
306
  end
307

308
  defimpl Enumerable do
309
    def count(map) do
1✔
310
      {:ok, Yex.Map.size(map)}
311
    end
312

313
    def member?(map, {key, value}) do
314
      value = Yex.normalize(value)
3✔
315

316
      case Yex.Map.fetch(map, key) do
3✔
317
        {:ok, ^value} -> {:ok, true}
1✔
318
        _ -> {:ok, false}
2✔
319
      end
320
    end
321

322
    def member?(_, _) do
1✔
323
      {:ok, false}
324
    end
325

326
    def slice(map) do
327
      list = Yex.Map.to_list(map)
1✔
328
      size = Enum.count(list)
1✔
329
      {:ok, size, &slice_impl(list, &1, &2, &3)}
1✔
330
    end
331

332
    defp slice_impl(list, start, length, step) do
333
      list
334
      |> Enum.slice(start, length)
335
      |> Enum.take_every(step)
1✔
336
    end
337

338
    def reduce(map, acc, fun) do
339
      Enumerable.List.reduce(Yex.Map.to_list(map), acc, fun)
1✔
340
    end
341
  end
342
end
343

344
defmodule Yex.MapPrelim do
345
  @moduledoc """
346
  A preliminary map representation used for initializing map content.
347
  This module provides functionality for creating map content before it is
348
  inserted into a shared document.
349

350
  ## Use Cases
351
  - Creating map content before inserting into a document
352
  - Serializing map content for transfer between documents
353
  - Initializing map content with specific key-value pairs
354
  - Preparing nested data structures for shared documents
355

356
  ## Examples
357
      iex> doc = Yex.Doc.new()
358
      iex> array = Yex.Doc.get_array(doc, "array")
359
      iex> Yex.Array.insert(array, 0, Yex.MapPrelim.from(%{ "key" => "value" }))
360
      iex> {:ok, %Yex.Map{} = map} = Yex.Array.fetch(array, 0)
361
      iex> Yex.Map.fetch(map, "key")
362
      {:ok, "value"}
363
  """
364
  defstruct [
365
    :map
366
  ]
367

368
  @type t :: %__MODULE__{
369
          map: %{binary() => Yex.input_type()}
370
        }
371

372
  @doc """
373
  Creates a new MapPrelim from an Elixir map.
374
  This is useful when you want to initialize a shared map with predefined content.
375

376
  ## Parameters
377
    * `map` - An Elixir map to convert to a preliminary map
378

379
  ## Examples
380
      iex> prelim = Yex.MapPrelim.from(%{"key" => "value", "nested" => %{"inner" => "data"}})
381
      iex> doc = Yex.Doc.new()
382
      iex> map = Yex.Doc.get_map(doc, "map")
383
      iex> Yex.Map.set(map, "content", prelim)
384
  """
385
  @spec from(%{binary() => Yex.input_type()}) :: t()
386
  def from(%{} = map) do
387
    %__MODULE__{map: map}
8✔
388
  end
389
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc