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

satoren / y_ex / a1db10bb0005a160334e352d052e8bcd4024bf29-PR-198

21 Nov 2025 01:55AM UTC coverage: 95.447% (-1.1%) from 96.534%
a1db10bb0005a160334e352d052e8bcd4024bf29-PR-198

Pull #198

github

satoren
version up to 0.10.0
Pull Request #198: Add *_and_get functions that return values directly

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

5 existing lines in 3 files 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 {:ok, content} on success, :error on failure.
47
  ## Parameters
48
    * `array` - The array to modify
49
    * `index` - The position to insert at (0-based)
50
    * `content` - The content to insert
51
  """
52
  @spec insert_and_get(t, integer(), Yex.input_type()) :: value()
53
  def insert_and_get(%__MODULE__{doc: doc} = array, index, content) when is_integer(index) do
54
    Doc.run_in_worker_process doc do
6✔
55
      index = if index < 0, do: __MODULE__.length(array) + index, else: index
6✔
56
      :ok = Yex.Nif.array_insert(array, cur_txn(array), index, content)
6✔
57

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

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

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

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

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

97
  @doc """
98
  Pushes content to the end of the array and returns the pushed content.
99
  Returns {:ok, content} on success, :error on failure.
100

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

259
  @doc """
260
  Returns as list
261

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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