• 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.31
/lib/shared_type/array.ex
1
defmodule Yex.Array do
2
  @moduledoc """
3
  A shareable Array-like type that supports efficient insert/delete of elements at any position.
4
  This module provides functionality for collaborative array manipulation with support for
5
  concurrent modifications and automatic conflict resolution.
6

7
  ## Features
8
  - Insert and delete elements at any position
9
  - Push and unshift operations for adding elements
10
  - Move elements between positions
11
  - Support for nested shared types
12
  - Automatic conflict resolution for concurrent modifications
13
  """
14
  defstruct [
15
    :doc,
16
    :reference
17
  ]
18

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

24
  @type value :: term()
25
  alias Yex.Doc
26
  require Yex.Doc
27

28
  @doc """
29
  Inserts content at the specified index.
30
  Returns :ok on success.
31

32
  ## Parameters
33
    * `array` - The array to modify
34
    * `index` - The position to insert at (0-based)
35
    * `content` - The content to insert
36
  """
37
  @spec insert(t, integer(), Yex.input_type()) :: :ok
38
  def insert(%__MODULE__{doc: doc} = array, index, content) when is_integer(index) do
39
    Doc.run_in_worker_process(doc,
146✔
40
      do: Yex.Nif.array_insert(array, cur_txn(array), index, content)
146✔
41
    )
42
  end
43

44
  @doc """
45
  Inserts content at the specified index and returns the inserted content.
46
  Returns the content on success, raises on failure.
47

48
  ## Parameters
49
    * `array` - The array to modify
50
    * `index` - The position to insert at (0-based)
51
    * `content` - The content to insert
52
  """
53
  @spec insert_and_get(t, integer(), Yex.input_type()) :: value()
54
  def insert_and_get(%__MODULE__{doc: doc} = array, index, content) when is_integer(index) do
55
    Doc.run_in_worker_process doc do
6✔
56
      index = if index < 0, do: __MODULE__.length(array) + index, else: index
6✔
57
      :ok = Yex.Nif.array_insert(array, cur_txn(array), index, content)
6✔
58

59
      case Yex.Nif.array_get(array, cur_txn(array), index) do
6✔
60
        {:ok, value} -> value
6✔
NEW
61
        :error -> raise RuntimeError, "Failed to get inserted value"
×
62
      end
63
    end
64
  end
65

66
  @doc """
67
  Insert contents at the specified index.
68

69
  ## Examples
70
      iex> doc = Yex.Doc.new()
71
      iex> array = Yex.Doc.get_array(doc, "array")
72
      iex> Yex.Array.insert_list(array, 0, [1,2,3,4,5])
73
      iex> Yex.Array.to_json(array)
74
      [1.0, 2.0, 3.0, 4.0, 5.0]
75
  """
76
  @spec insert_list(t, integer(), list(Yex.any_type())) :: :ok
77
  def insert_list(%__MODULE__{doc: doc} = array, index, contents) when is_integer(index) do
78
    Doc.run_in_worker_process(doc,
17✔
79
      do: Yex.Nif.array_insert_list(array, cur_txn(array), index, contents)
17✔
80
    )
81
  end
82

83
  @doc """
84
  Pushes content to the end of the array.
85
  Returns :ok on success, :error on failure.
86

87
  ## Parameters
88
    * `array` - The array to modify
89
    * `content` - The content to append
90
  """
91
  @spec push(t, Yex.input_type()) :: :ok
92
  def push(%__MODULE__{doc: doc} = array, content) do
93
    Doc.run_in_worker_process(doc,
90✔
94
      do: insert(array, __MODULE__.length(array), content)
90✔
95
    )
96
  end
97

98
  @doc """
99
  Pushes content to the end of the array and returns the pushed content.
100
  Returns the content on success, raises on failure.
101

102
  ## Parameters
103
    * `array` - The array to modify
104
    * `content` - The content to append
105

106
  ## Examples
107
      iex> doc = Yex.Doc.new()
108
      iex> array = Yex.Doc.get_array(doc, "array")
109
      iex> value = Yex.Array.push_and_get(array, "Hello")
110
      iex> value
111
      "Hello"
112
  """
113
  @spec push_and_get(t, Yex.input_type()) :: value()
114
  def push_and_get(%__MODULE__{doc: doc} = array, content) do
115
    Doc.run_in_worker_process doc do
6✔
116
      index = __MODULE__.length(array)
6✔
117
      :ok = Yex.Nif.array_insert(array, cur_txn(array), index, content)
6✔
118

119
      case Yex.Nif.array_get(array, cur_txn(array), index) do
6✔
120
        {:ok, value} -> value
6✔
NEW
121
        :error -> raise RuntimeError, "Failed to get pushed value"
×
122
      end
123
    end
124
  end
125

126
  @doc """
127
  Unshifts content to the beginning of the array.
128
  Returns :ok on success, :error on failure.
129

130
  ## Parameters
131
    * `array` - The array to modify
132
    * `content` - The content to prepend
133
  """
134
  def unshift(%__MODULE__{} = array, content) do
135
    insert(array, 0, content)
2✔
136
  end
137

138
  @doc """
139
  Deletes content at the specified index.
140
  Returns :ok on success, :error on failure.
141

142
  ## Parameters
143
    * `array` - The array to modify
144
    * `index` - The position to delete from (0-based)
145
  """
146
  @spec delete(t, integer()) :: :ok
147
  def delete(%__MODULE__{} = array, index) do
148
    delete_range(array, index, 1)
7✔
149
  end
150

151
  @doc """
152
  Deletes a range of contents starting at the specified index.
153
  Returns :ok on success, :error on failure.
154

155
  ## Parameters
156
    * `array` - The array to modify
157
    * `index` - The starting position to delete from (0-based)
158
    * `length` - The number of elements to delete
159
  """
160
  @spec delete_range(t, integer(), integer()) :: :ok
161
  def delete_range(%__MODULE__{doc: doc} = array, index, length)
162
      when is_integer(index) and is_integer(length) do
163
    Doc.run_in_worker_process doc do
11✔
164
      index = if index < 0, do: __MODULE__.length(array) + index, else: index
11✔
165
      Yex.Nif.array_delete_range(array, cur_txn(array), index, length)
11✔
166
    end
167
  end
168

169
  @doc """
170
  Moves element found at `source` index into `target` index position. Both indexes refer to a current state of the document.
171
  ## Examples pushes a string then fetches it back
172
      iex> doc = Yex.Doc.new()
173
      iex> array = Yex.Doc.get_array(doc, "array")
174
      iex> Yex.Array.push(array, Yex.ArrayPrelim.from([1, 2]))
175
      iex> Yex.Array.push(array, Yex.ArrayPrelim.from([3, 4]))
176
      iex> :ok = Yex.Array.move_to(array, 0, 2)
177
      iex> Yex.Array.to_json(array)
178
      [[3.0, 4.0], [1.0, 2.0]]
179
  """
180
  @spec move_to(t, integer(), integer()) :: :ok
181
  def move_to(%__MODULE__{doc: doc} = array, from, to) when is_integer(from) and is_integer(to) do
182
    Doc.run_in_worker_process(doc,
10✔
183
      do: Yex.Nif.array_move_to(array, cur_txn(array), from, to)
10✔
184
    )
185
  end
186

187
  @spec get(t, integer(), default :: value()) :: value()
188
  def get(array, index, default \\ nil) do
189
    case fetch(array, index) do
3✔
190
      {:ok, value} -> value
1✔
191
      :error -> default
2✔
192
    end
193
  end
194

195
  @doc """
196
  Gets a value by index from the array, or lazily evaluates the given function if the index is out of bounds.
197
  This is useful when the default value is expensive to compute and should only be evaluated when needed.
198

199
  ## Parameters
200
    * `array` - The array to query
201
    * `index` - The index to look up
202
    * `fun` - A function that returns the default value (only called if index is out of bounds)
203

204
  ## Examples
205
      iex> doc = Yex.Doc.new()
206
      iex> array = Yex.Doc.get_array(doc, "array")
207
      iex> Yex.Array.push(array, "Hello")
208
      iex> Yex.Array.get_lazy(array, 0, fn -> "Default" end)
209
      "Hello"
210
      iex> Yex.Array.get_lazy(array, 10, fn -> "Computed" end)
211
      "Computed"
212

213
  Particularly useful with `insert_and_get/3` or `push_and_get/2` for get-or-create patterns:
214

215
      iex> doc = Yex.Doc.new()
216
      iex> array = Yex.Doc.get_array(doc, "array")
217
      iex> # Get existing value or create and return new one
218
      iex> value = Yex.Array.get_lazy(array, 0, fn ->
219
      ...>   Yex.Array.push_and_get(array, "initial")
220
      ...> end)
221
      iex> value
222
      "initial"
223
      iex> # Next call returns existing value without calling the function
224
      iex> Yex.Array.get_lazy(array, 0, fn -> Yex.Array.push_and_get(array, "initial") end)
225
      "initial"
226
  """
227
  @spec get_lazy(t, integer(), fun :: (-> value())) :: value()
228
  def get_lazy(%__MODULE__{} = array, index, fun) do
229
    case fetch(array, index) do
6✔
230
      {:ok, value} -> value
3✔
231
      :error -> fun.()
3✔
232
    end
233
  end
234

235
  @doc """
236
  Get content at the specified index.
237
  ## Examples pushes a string then fetches it back
238
      iex> doc = Yex.Doc.new()
239
      iex> array = Yex.Doc.get_array(doc, "array")
240
      iex> Yex.Array.push(array, "Hello")
241
      iex> Yex.Array.fetch(array, 0)
242
      {:ok, "Hello"}
243
  """
244
  @spec fetch(t, integer()) :: {:ok, value()} | :error
245
  def fetch(%__MODULE__{doc: doc} = array, index) when is_integer(index) do
246
    Doc.run_in_worker_process doc do
36✔
247
      index = if index < 0, do: __MODULE__.length(array) + index, else: index
36✔
248
      Yex.Nif.array_get(array, cur_txn(array), index)
36✔
249
    end
250
  end
251

252
  @spec fetch!(t, integer()) :: value()
253
  def fetch!(%__MODULE__{} = array, index) when is_integer(index) do
254
    case fetch(array, index) do
3✔
255
      {:ok, value} -> value
2✔
256
      :error -> raise ArgumentError, "Index out of bounds"
1✔
257
    end
258
  end
259

260
  @doc """
261
  Returns as list
262

263
  ## Examples adds a few items to an array, then gets them back as Elixir List
264
      iex> doc = Yex.Doc.new()
265
      iex> array = Yex.Doc.get_array(doc, "array")
266
      iex> Yex.Array.push(array, "Hello")
267
      iex> Yex.Array.push(array, "World")
268
      iex> Yex.Array.push(array, Yex.ArrayPrelim.from([1, 2]))
269
      iex> ["Hello", "World", %Yex.Array{}] = Yex.Array.to_list(array)
270

271
  """
272
  def to_list(%__MODULE__{doc: doc} = array) do
273
    Doc.run_in_worker_process doc do
55✔
274
      Yex.Nif.array_to_list(array, cur_txn(array))
55✔
275
    end
276
  end
277

278
  @doc """
279
   slices the array from start_index for amount of elements, then gets them back as Elixir List.
280
  """
281
  def slice(array, start_index, amount) do
282
    slice_take_every(array, start_index, amount, 1)
3✔
283
  end
284

285
  @doc """
286
    slices the array from start_index for amount of elements, then gets them back as Elixir List and takes every `step` element.
287
  """
288
  def slice_take_every(_array, _start_index, _amount, 0) do
1✔
289
    []
290
  end
291

292
  def slice_take_every(%__MODULE__{doc: doc} = array, start_index, amount, step)
293
      when is_integer(step) and step > 0 do
294
    Doc.run_in_worker_process doc do
9✔
295
      Yex.Nif.array_slice(array, cur_txn(array), start_index, amount, step)
9✔
296
    end
297
  end
298

299
  @doc """
300
  Returns the length of the array
301

302
  ## Examples adds a few items to an array and returns its length
303
      iex> doc = Yex.Doc.new()
304
      iex> array = Yex.Doc.get_array(doc, "array")
305
      iex> Yex.Array.push(array, "Hello")
306
      iex> Yex.Array.push(array, "World")
307
      iex> Yex.Array.length(array)
308
      2
309
  """
310
  def length(%__MODULE__{doc: doc} = array) do
311
    Doc.run_in_worker_process doc do
121✔
312
      Yex.Nif.array_length(array, cur_txn(array))
121✔
313
    end
314
  end
315

316
  @doc """
317
  Convert to json-compatible format.
318

319
  ## Examples adds a few items to an array and returns as Elixir List
320
      iex> doc = Yex.Doc.new()
321
      iex> array = Yex.Doc.get_array(doc, "array")
322
      iex> Yex.Array.push(array, "Hello")
323
      iex> Yex.Array.push(array, "World")
324
      iex> Yex.Array.to_json(array)
325
      ["Hello", "World"]
326
  """
327
  @spec to_json(t) :: term()
328
  def to_json(%__MODULE__{doc: doc} = array) do
329
    Doc.run_in_worker_process doc do
29✔
330
      Yex.Nif.array_to_json(array, cur_txn(array))
29✔
331
    end
332
  end
333

334
  def member?(array, val) do
335
    val = Yex.normalize(val)
7✔
336
    Enum.member?(to_list(array), val)
7✔
337
  end
338

339
  defp cur_txn(%{doc: %Yex.Doc{reference: doc_ref}}) do
340
    Process.get(doc_ref, nil)
458✔
341
  end
342

343
  @doc """
344
  Converts the array to its preliminary representation.
345
  This is useful when you need to serialize or transfer the array's contents.
346

347
  ## Parameters
348
    * `array` - The array to convert
349
  """
350
  @spec as_prelim(t) :: Yex.ArrayPrelim.t()
351
  def as_prelim(%__MODULE__{doc: doc} = array) do
352
    Doc.run_in_worker_process(doc,
6✔
353
      do:
354
        Yex.Array.to_list(array)
355
        |> Enum.map(&Yex.Output.as_prelim/1)
356
        |> Yex.ArrayPrelim.from()
6✔
357
    )
358
  end
359

360
  defimpl Yex.Output do
361
    def as_prelim(array) do
362
      Yex.Array.as_prelim(array)
3✔
363
    end
364
  end
365

366
  defimpl Enumerable do
367
    def count(array) do
2✔
368
      {:ok, Yex.Array.length(array)}
369
    end
370

371
    def member?(array, val) do
4✔
372
      {:ok, Yex.Array.member?(array, val)}
373
    end
374

375
    def slice(array) do
376
      size = Yex.Array.length(array)
12✔
377
      {:ok, size, &slice_impl(array, &1, &2, &3)}
12✔
378
    end
379

380
    defp slice_impl(array, start, length, step) do
381
      # Optimize for single element access (Enum.at)
382
      if length == 1 and step == 1 do
9✔
383
        case Yex.Array.fetch(array, start) do
8✔
384
          {:ok, value} -> [value]
8✔
385
          :error -> []
×
386
        end
387
      else
388
        Yex.Array.slice_take_every(array, start, length, step)
1✔
389
      end
390
    end
391

392
    def reduce(array, acc, fun) do
393
      Enumerable.List.reduce(Yex.Array.to_list(array), acc, fun)
5✔
394
    end
395
  end
396
end
397

398
defmodule Yex.ArrayPrelim do
399
  @moduledoc """
400
  A preliminary array. It can be used to early initialize the contents of a Array.
401

402
  ## Examples
403
      iex> doc = Yex.Doc.new()
404
      iex> map = Yex.Doc.get_map(doc, "map")
405
      iex> Yex.Map.set(map, "key", Yex.ArrayPrelim.from(["Hello", "World"]))
406
      iex> {:ok, %Yex.Array{} = array} = Yex.Map.fetch(map, "key")
407
      iex> Yex.Array.fetch(array, 1)
408
      {:ok, "World"}
409

410
  """
411
  defstruct [
412
    :list
413
  ]
414

415
  @type t :: %__MODULE__{
416
          list: list(Yex.input_type())
417
        }
418

419
  @spec from(Enumerable.t(Yex.input_type())) :: t
420
  def from(enumerable) do
421
    %__MODULE__{list: Enum.to_list(enumerable)}
27✔
422
  end
423
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