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

satoren / y_ex / 7eed24bdcecbf7d15513dce2d288718fac0ad72e

21 Nov 2025 02:16AM UTC coverage: 95.447% (-1.1%) from 96.534%
7eed24bdcecbf7d15513dce2d288718fac0ad72e

push

github

web-flow
Add *_and_get functions that return values directly (#198)

* Add *_and_get functions that return values directly and update get functions

BREAKING CHANGES:
1. *_and_get functions now return values directly instead of {:ok, value} tuples
   - Modified insert_and_get, push_and_get in Yex.Array
   - Modified set_and_get in Yex.Map
   - Modified insert_and_get, insert_after_and_get, push_and_get in Yex.XmlElement
   - Modified insert_and_get, insert_after_and_get, push_and_get in Yex.XmlFragment
   - Changed error handling to raise exceptions instead of returning :error

2. Previously deprecated get functions are no longer deprecated
   - get/2 and get/3 now return values directly instead of {:ok, value} tuples
   - Updated in Yex.Array, Yex.Map, Yex.XmlElement, Yex.XmlFragment

New features:
- Added get_lazy/3 functions to all affected modules for lazy default value evaluation

Updates:
- Updated type specifications to return term() directly
- Updated all test files to match new API

* version up to 0.10.0

* fix

* fix doc and test

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

587 of 615 relevant lines covered (95.45%)

22.38 hits per line

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

96.34
/lib/shared_type/xml_element.ex
1
defmodule Yex.XmlElement do
2
  @moduledoc """
3
  A shared type that represents an XML node.
4
  Provides functionality for manipulating XML elements including child nodes, attributes, and navigation.
5

6
  """
7

8
  alias Yex.Xml
9

10
  defstruct [
11
    :doc,
12
    :reference
13
  ]
14

15
  alias Yex.Doc
16
  require Yex.Doc
17

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

23
  @doc """
24
  Returns the first child node of the XML element.
25
  Returns nil if the element has no children.
26
  """
27
  @spec first_child(t) :: Yex.XmlElement.t() | Yex.XmlText.t() | nil
28
  def first_child(%__MODULE__{} = xml_element) do
29
    fetch(xml_element, 0)
30
    |> case do
30✔
31
      {:ok, node} -> node
22✔
32
      :error -> nil
8✔
33
    end
34
  end
35

36
  @doc """
37
  Returns a stream of all child nodes of the XML element.
38
  """
39
  @spec children(t) :: Enumerable.t(Yex.XmlElement.t() | Yex.XmlText.t())
40
  def children(%__MODULE__{doc: doc} = xml_element) do
41
    Doc.run_in_worker_process(doc,
23✔
42
      do:
43
        Stream.unfold(first_child(xml_element), fn
23✔
44
          nil -> nil
20✔
45
          xml -> {xml, Xml.next_sibling(xml)}
38✔
46
        end)
47
    )
48
  end
49

50
  @doc """
51
  Returns the number of child nodes in the XML element.
52
  """
53
  @spec length(t) :: integer()
54
  def length(%__MODULE__{doc: doc} = xml_element) do
55
    Doc.run_in_worker_process(doc,
71✔
56
      do: Yex.Nif.xml_element_length(xml_element, cur_txn(xml_element))
71✔
57
    )
58
  end
59

60
  @doc """
61
  Inserts a new child node at the specified index.
62
  Returns :ok on success, :error on failure.
63
  """
64
  @spec insert(t, integer(), Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()) :: :ok | :error
65
  def insert(%__MODULE__{doc: doc} = xml_element, index, content) do
66
    Doc.run_in_worker_process(doc,
71✔
67
      do: Yex.Nif.xml_element_insert(xml_element, cur_txn(xml_element), index, content)
71✔
68
    )
69
  end
70

71
  @doc """
72
  Inserts a new child node at the specified index and returns the inserted node.
73
  Returns the inserted node on success, raises on failure.
74

75
  """
76
  @spec insert_and_get(t, integer(), Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()) ::
77
          Yex.XmlElement.t() | Yex.XmlText.t()
78
  def insert_and_get(%__MODULE__{doc: doc} = xml_element, index, content) do
79
    Doc.run_in_worker_process doc do
4✔
80
      :ok = Yex.Nif.xml_element_insert(xml_element, cur_txn(xml_element), index, content)
4✔
81

82
      case Yex.Nif.xml_element_get(xml_element, cur_txn(xml_element), index) do
4✔
83
        {:ok, value} -> value
4✔
NEW
84
        :error -> raise RuntimeError, "Failed to get inserted XML element"
×
85
      end
86
    end
87
  end
88

89
  @doc """
90
  Inserts a new child node after the specified reference node.
91
  If the reference node is not found, inserts at the beginning.
92
  Returns :ok on success, :error on failure.
93
  """
94
  @spec insert_after(
95
          t,
96
          Yex.XmlElement.t() | Yex.XmlText.t(),
97
          Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()
98
        ) :: :ok | :error
99
  def insert_after(%__MODULE__{doc: doc} = xml_element, ref, content) do
100
    Doc.run_in_worker_process doc do
4✔
101
      index = children(xml_element) |> Enum.find_index(&(&1 == ref))
4✔
102

103
      if index == nil do
4✔
104
        insert(xml_element, 0, content)
2✔
105
      else
106
        insert(xml_element, index + 1, content)
2✔
107
      end
108
    end
109
  end
110

111
  @doc """
112
  Inserts a new child node after the specified reference node and returns the inserted node.
113
  If the reference node is not found, inserts at the beginning.
114
  Returns the inserted node on success, raises on failure.
115

116
  """
117
  @spec insert_after_and_get(
118
          t,
119
          Yex.XmlElement.t() | Yex.XmlText.t(),
120
          Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()
121
        ) :: Yex.XmlElement.t() | Yex.XmlText.t()
122
  def insert_after_and_get(%__MODULE__{doc: doc} = xml_element, ref, content) do
123
    Doc.run_in_worker_process doc do
2✔
124
      index = children(xml_element) |> Enum.find_index(&(&1 == ref))
2✔
125

126
      target_index =
2✔
127
        if index == nil do
1✔
128
          0
129
        else
130
          index + 1
1✔
131
        end
132

133
      :ok = insert(xml_element, target_index, content)
2✔
134

135
      case fetch(xml_element, target_index) do
2✔
136
        {:ok, value} -> value
2✔
NEW
137
        :error -> raise RuntimeError, "Failed to get inserted XML element"
×
138
      end
139
    end
140
  end
141

142
  @doc """
143
  Deletes a range of child nodes starting at the specified index.
144
  Returns :ok on success, :error on failure.
145
  """
146
  @spec delete(t, integer(), integer()) :: :ok | :error
147
  def delete(%__MODULE__{doc: doc} = xml_element, index, length) do
148
    Doc.run_in_worker_process(doc,
6✔
149
      do: Yex.Nif.xml_element_delete_range(xml_element, cur_txn(xml_element), index, length)
6✔
150
    )
151
  end
152

153
  @doc """
154
  Appends a new child node at the end of the children list.
155
  Returns :ok on success, :error on failure.
156
  """
157
  @spec push(t, Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()) :: :ok | :error
158
  def push(%__MODULE__{doc: doc} = xml_element, content) do
159
    Doc.run_in_worker_process(doc,
50✔
160
      do: insert(xml_element, __MODULE__.length(xml_element), content)
50✔
161
    )
162
  end
163

164
  @doc """
165
  Appends a new child node at the end of the children list and returns the inserted node.
166
  Returns the inserted node on success, raises on failure.
167
  """
168
  @spec push_and_get(t, Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()) ::
169
          Yex.XmlElement.t() | Yex.XmlText.t()
170
  def push_and_get(%__MODULE__{doc: doc} = xml_element, content) do
171
    Doc.run_in_worker_process doc do
5✔
172
      index = __MODULE__.length(xml_element)
5✔
173
      :ok = insert(xml_element, index, content)
5✔
174

175
      case fetch(xml_element, index) do
5✔
176
        {:ok, value} -> value
5✔
NEW
177
        :error -> raise RuntimeError, "Failed to get pushed XML element"
×
178
      end
179
    end
180
  end
181

182
  @doc """
183
  Inserts a new child node at the beginning of the children list.
184
  Returns :ok on success, :error on failure.
185
  """
186
  @spec unshift(t, Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()) :: :ok | :error
187
  def unshift(%__MODULE__{} = xml_element, content) do
188
    insert(xml_element, 0, content)
2✔
189
  end
190

191
  @doc """
192
  Gets a child node by index from the XML element, or returns the default value if the index is out of bounds.
193

194
  ## Parameters
195
    * `xml_element` - The XML element to query
196
    * `index` - The index to look up
197
    * `default` - The default value to return if index is out of bounds (defaults to nil)
198

199
  ## Examples
200
      iex> doc = Yex.Doc.new()
201
      iex> xml = Yex.Doc.get_xml_fragment(doc, "xml")
202
      iex> elem = Yex.XmlFragment.push_and_get(xml, Yex.XmlElementPrelim.empty("div"))
203
      iex> Yex.XmlElement.push(elem, Yex.XmlTextPrelim.from("content"))
204
      iex> text = Yex.XmlElement.get(elem, 0)
205
      iex> match?(%Yex.XmlText{}, text)
206
      true
207
      iex> Yex.XmlElement.get(elem, 10)
208
      nil
209
      iex> Yex.XmlElement.get(elem, 10, :not_found)
210
      :not_found
211
  """
212
  @spec get(t, integer(), default :: term()) :: Yex.XmlElement.t() | Yex.XmlText.t() | term()
213
  def get(%__MODULE__{} = xml_element, index, default \\ nil) do
214
    case fetch(xml_element, index) do
7✔
215
      {:ok, value} -> value
3✔
216
      :error -> default
4✔
217
    end
218
  end
219

220
  @doc """
221
  Gets a child node by index from the XML element, or lazily evaluates the given function if the index is out of bounds.
222
  This is useful when the default value is expensive to compute and should only be evaluated when needed.
223

224
  ## Parameters
225
    * `xml_element` - The XML element to query
226
    * `index` - The index to look up
227
    * `fun` - A function that returns the default value (only called if index is out of bounds)
228

229
  ## Examples
230
      iex> doc = Yex.Doc.new()
231
      iex> xml = Yex.Doc.get_xml_fragment(doc, "xml")
232
      iex> elem = Yex.XmlFragment.push_and_get(xml, Yex.XmlElementPrelim.empty("div"))
233
      iex> Yex.XmlElement.push(elem, Yex.XmlTextPrelim.from("content"))
234
      iex> text = Yex.XmlElement.get_lazy(elem, 0, fn -> Yex.XmlTextPrelim.from("default") end)
235
      iex> match?(%Yex.XmlText{}, text)
236
      true
237

238
  Particularly useful with `*_and_get` functions for get-or-create patterns:
239

240
      iex> doc = Yex.Doc.new()
241
      iex> xml = Yex.Doc.get_xml_fragment(doc, "xml")
242
      iex> elem = Yex.XmlFragment.push_and_get(xml, Yex.XmlElementPrelim.empty("div"))
243
      iex> # Get existing child or create and return new one
244
      iex> child = Yex.XmlElement.get_lazy(elem, 0, fn ->
245
      ...>   Yex.XmlElement.push_and_get(elem, Yex.XmlElementPrelim.empty("span"))
246
      ...> end)
247
      iex> Yex.XmlElement.get_tag(child)
248
      "span"
249
  """
250
  @spec get_lazy(t, integer(), fun :: (-> term())) ::
251
          Yex.XmlElement.t() | Yex.XmlText.t() | term()
252
  def get_lazy(%__MODULE__{} = xml_element, index, fun) do
253
    case fetch(xml_element, index) do
9✔
254
      {:ok, value} -> value
4✔
255
      :error -> fun.()
5✔
256
    end
257
  end
258

259
  @doc """
260
  Retrieves the child node at the specified index.
261
  Returns {:ok, node} if found, :error if index is out of bounds.
262
  """
263
  @spec fetch(t, integer()) :: {:ok, Yex.XmlElement.t() | Yex.XmlText.t()} | :error
264
  def fetch(%__MODULE__{doc: doc} = xml_element, index) do
265
    Doc.run_in_worker_process(doc,
76✔
266
      do: Yex.Nif.xml_element_get(xml_element, cur_txn(xml_element), index)
76✔
267
    )
268
  end
269

270
  @doc """
271
  Similar to fetch/2 but raises ArgumentError if the index is out of bounds.
272
  """
273
  @spec fetch!(t, integer()) :: Yex.XmlElement.t() | Yex.XmlText.t()
274
  def fetch!(%__MODULE__{} = map, index) do
275
    case fetch(map, index) do
12✔
276
      {:ok, value} -> value
10✔
277
      :error -> raise ArgumentError, "Index out of bounds"
2✔
278
    end
279
  end
280

281
  @doc """
282
  Adds or updates an attribute with the specified key and value.
283
  Returns :ok on success, :error on failure.
284
  """
285
  @spec insert_attribute(t, binary(), binary() | Yex.PrelimType.t()) :: :ok | :error
286
  def insert_attribute(%__MODULE__{doc: doc} = xml_element, key, value) do
287
    Doc.run_in_worker_process(doc,
21✔
288
      do: Yex.Nif.xml_element_insert_attribute(xml_element, cur_txn(xml_element), key, value)
21✔
289
    )
290
  end
291

292
  @doc """
293
  Removes the attribute with the specified key.
294
  Returns :ok on success, :error on failure.
295
  """
296
  @spec remove_attribute(t, binary()) :: :ok | :error
297
  def remove_attribute(%__MODULE__{doc: doc} = xml_element, key) do
298
    Doc.run_in_worker_process(doc,
3✔
299
      do: Yex.Nif.xml_element_remove_attribute(xml_element, cur_txn(xml_element), key)
3✔
300
    )
301
  end
302

303
  @doc """
304
  Returns the tag name of the XML element.
305
  Returns nil if the element has no tag.
306
  """
307
  @spec get_tag(t) :: binary() | nil
308
  def get_tag(%__MODULE__{doc: doc} = xml_element) do
309
    Doc.run_in_worker_process(doc,
19✔
310
      do: Yex.Nif.xml_element_get_tag(xml_element, cur_txn(xml_element))
19✔
311
    )
312
  end
313

314
  @doc """
315
  Returns the value of the specified attribute.
316
  Returns nil if the attribute does not exist.
317
  """
318
  @spec get_attribute(t, binary()) :: binary() | Yex.SharedType.t() | nil
319
  def get_attribute(%__MODULE__{doc: doc} = xml_element, key) do
320
    Doc.run_in_worker_process(doc,
8✔
321
      do: Yex.Nif.xml_element_get_attribute(xml_element, cur_txn(xml_element), key)
8✔
322
    )
323
  end
324

325
  @doc """
326
  Returns a map of all attributes for this XML element.
327
  """
328
  @spec get_attributes(t) :: %{binary() => binary() | Yex.SharedType.t()}
329
  def get_attributes(%__MODULE__{doc: doc} = xml_element) do
330
    Doc.run_in_worker_process(doc,
16✔
331
      do: Yex.Nif.xml_element_get_attributes(xml_element, cur_txn(xml_element))
16✔
332
    )
333
  end
334

335
  @doc """
336
  The next sibling of this type. Is null if this is the last child of its parent.
337
  """
338
  @spec next_sibling(t) :: Yex.XmlElement.t() | Yex.XmlText.t() | nil
339
  def next_sibling(%__MODULE__{doc: doc} = xml_element) do
340
    Doc.run_in_worker_process(doc,
25✔
341
      do: Yex.Nif.xml_element_next_sibling(xml_element, cur_txn(xml_element))
25✔
342
    )
343
  end
344

345
  @doc """
346
  The previous sibling of this type. Is null if this is the first child of its parent.
347
  """
348
  @spec prev_sibling(t) :: Yex.XmlElement.t() | Yex.XmlText.t() | nil
349
  def prev_sibling(%__MODULE__{doc: doc} = xml_element) do
350
    Doc.run_in_worker_process(doc,
5✔
351
      do: Yex.Nif.xml_element_prev_sibling(xml_element, cur_txn(xml_element))
5✔
352
    )
353
  end
354

355
  @doc """
356
  The parent that holds this type. Is null if this xml is a top-level XML type.
357
  """
358
  @spec parent(t) :: Yex.XmlElement.t() | Yex.XmlFragment.t() | nil
359
  def parent(%__MODULE__{doc: doc} = xml_element) do
360
    Doc.run_in_worker_process(doc,
4✔
361
      do: Yex.Nif.xml_element_parent(xml_element, cur_txn(xml_element))
4✔
362
    )
363
  end
364

365
  @spec to_string(t) :: binary()
366
  def to_string(%__MODULE__{doc: doc} = xml_element) do
367
    Doc.run_in_worker_process(doc,
9✔
368
      do: Yex.Nif.xml_element_to_string(xml_element, cur_txn(xml_element))
9✔
369
    )
370
  end
371

372
  defp cur_txn(%{doc: %Yex.Doc{reference: doc_ref}}) do
373
    Process.get(doc_ref, nil)
342✔
374
  end
375

376
  @spec as_prelim(t) :: Yex.XmlElementPrelim.t()
377
  def as_prelim(%__MODULE__{doc: doc} = xml_element) do
378
    Doc.run_in_worker_process doc do
12✔
379
      c =
12✔
380
        children(xml_element)
381
        |> Enum.map(fn child -> Yex.Output.as_prelim(child) end)
10✔
382

383
      Yex.XmlElementPrelim.new(
12✔
384
        get_tag(xml_element),
385
        c,
386
        get_attributes(xml_element)
387
      )
388
    end
389
  end
390

391
  defimpl Yex.Output do
392
    def as_prelim(xml_element) do
393
      Yex.XmlElement.as_prelim(xml_element)
7✔
394
    end
395
  end
396

397
  defimpl Yex.Xml do
398
    defdelegate next_sibling(xml), to: Yex.XmlElement
24✔
399
    defdelegate prev_sibling(xml), to: Yex.XmlElement
4✔
400
    defdelegate parent(xml), to: Yex.XmlElement
1✔
401
    defdelegate to_string(xml), to: Yex.XmlElement
1✔
402
  end
403
end
404

405
defmodule Yex.XmlElementPrelim do
406
  @moduledoc """
407
  A preliminary xml element. It can be used to early initialize the contents of a XmlElement.
408

409
  ## Examples
410
      iex> doc = Yex.Doc.new()
411
      iex> xml = Yex.Doc.get_xml_fragment(doc, "xml")
412
      iex> Yex.XmlFragment.insert(xml, 0,  Yex.XmlElementPrelim.empty("div"))
413
      iex> Yex.XmlFragment.to_string(xml)
414
      "<div></div>"
415

416
  """
417
  defstruct [:tag, :attributes, :children]
418

419
  @type t :: %__MODULE__{
420
          tag: String.t(),
421
          attributes: %{String.t() => String.t() | Yex.PrelimType.t()},
422
          children: [Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()]
423
        }
424

425
  def new(tag, children, attributes \\ %{}) do
426
    %__MODULE__{
25✔
427
      tag: tag,
428
      attributes: attributes,
429
      children: Enum.to_list(children)
430
    }
431
  end
432

433
  def empty(tag) do
434
    %__MODULE__{
129✔
435
      tag: tag,
436
      attributes: %{},
437
      children: []
438
    }
439
  end
440
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc