• 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

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,
70✔
56
      do: Yex.Nif.xml_element_length(xml_element, cur_txn(xml_element))
70✔
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,
70✔
67
      do: Yex.Nif.xml_element_insert(xml_element, cur_txn(xml_element), index, content)
70✔
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,
49✔
160
      do: insert(xml_element, __MODULE__.length(xml_element), content)
49✔
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
  @spec get(t, integer(), default :: term()) :: Yex.XmlElement.t() | Yex.XmlText.t() | term()
192
  def get(%__MODULE__{} = xml_element, index, default \\ nil) do
193
    case fetch(xml_element, index) do
4✔
194
      {:ok, value} -> value
2✔
195
      :error -> default
2✔
196
    end
197
  end
198

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

203
  ## Parameters
204
    * `xml_element` - The XML element to query
205
    * `index` - The index to look up
206
    * `fun` - A function that returns the default value (only called if index is out of bounds)
207

208
  ## Examples
209
      iex> doc = Yex.Doc.new()
210
      iex> xml = Yex.Doc.get_xml_fragment(doc, "xml")
211
      iex> elem = Yex.XmlFragment.push_and_get(xml, Yex.XmlElementPrelim.empty("div"))
212
      iex> Yex.XmlElement.push(elem, Yex.XmlTextPrelim.from("content"))
213
      iex> text = Yex.XmlElement.get_lazy(elem, 0, fn -> Yex.XmlTextPrelim.from("default") end)
214
      iex> match?(%Yex.XmlText{}, text)
215
      true
216

217
  Particularly useful with `*_and_get` functions for get-or-create patterns:
218

219
      iex> doc = Yex.Doc.new()
220
      iex> xml = Yex.Doc.get_xml_fragment(doc, "xml")
221
      iex> elem = Yex.XmlFragment.push_and_get(xml, Yex.XmlElementPrelim.empty("div"))
222
      iex> # Get existing child or create and return new one
223
      iex> child = Yex.XmlElement.get_lazy(elem, 0, fn ->
224
      ...>   Yex.XmlElement.push_and_get(elem, Yex.XmlElementPrelim.empty("span"))
225
      ...> end)
226
      iex> Yex.XmlElement.get_tag(child)
227
      "span"
228
  """
229
  @spec get_lazy(t, integer(), fun :: (-> term())) ::
230
          Yex.XmlElement.t() | Yex.XmlText.t() | term()
231
  def get_lazy(%__MODULE__{} = xml_element, index, fun) do
232
    case fetch(xml_element, index) do
9✔
233
      {:ok, value} -> value
4✔
234
      :error -> fun.()
5✔
235
    end
236
  end
237

238
  @doc """
239
  Retrieves the child node at the specified index.
240
  Returns {:ok, node} if found, :error if index is out of bounds.
241
  """
242
  @spec fetch(t, integer()) :: {:ok, Yex.XmlElement.t() | Yex.XmlText.t()} | :error
243
  def fetch(%__MODULE__{doc: doc} = xml_element, index) do
244
    Doc.run_in_worker_process(doc,
73✔
245
      do: Yex.Nif.xml_element_get(xml_element, cur_txn(xml_element), index)
73✔
246
    )
247
  end
248

249
  @doc """
250
  Similar to fetch/2 but raises ArgumentError if the index is out of bounds.
251
  """
252
  @spec fetch!(t, integer()) :: Yex.XmlElement.t() | Yex.XmlText.t()
253
  def fetch!(%__MODULE__{} = map, index) do
254
    case fetch(map, index) do
12✔
255
      {:ok, value} -> value
10✔
256
      :error -> raise ArgumentError, "Index out of bounds"
2✔
257
    end
258
  end
259

260
  @doc """
261
  Adds or updates an attribute with the specified key and value.
262
  Returns :ok on success, :error on failure.
263
  """
264
  @spec insert_attribute(t, binary(), binary() | Yex.PrelimType.t()) :: :ok | :error
265
  def insert_attribute(%__MODULE__{doc: doc} = xml_element, key, value) do
266
    Doc.run_in_worker_process(doc,
21✔
267
      do: Yex.Nif.xml_element_insert_attribute(xml_element, cur_txn(xml_element), key, value)
21✔
268
    )
269
  end
270

271
  @doc """
272
  Removes the attribute with the specified key.
273
  Returns :ok on success, :error on failure.
274
  """
275
  @spec remove_attribute(t, binary()) :: :ok | :error
276
  def remove_attribute(%__MODULE__{doc: doc} = xml_element, key) do
277
    Doc.run_in_worker_process(doc,
3✔
278
      do: Yex.Nif.xml_element_remove_attribute(xml_element, cur_txn(xml_element), key)
3✔
279
    )
280
  end
281

282
  @doc """
283
  Returns the tag name of the XML element.
284
  Returns nil if the element has no tag.
285
  """
286
  @spec get_tag(t) :: binary() | nil
287
  def get_tag(%__MODULE__{doc: doc} = xml_element) do
288
    Doc.run_in_worker_process(doc,
19✔
289
      do: Yex.Nif.xml_element_get_tag(xml_element, cur_txn(xml_element))
19✔
290
    )
291
  end
292

293
  @doc """
294
  Returns the value of the specified attribute.
295
  Returns nil if the attribute does not exist.
296
  """
297
  @spec get_attribute(t, binary()) :: binary() | Yex.SharedType.t() | nil
298
  def get_attribute(%__MODULE__{doc: doc} = xml_element, key) do
299
    Doc.run_in_worker_process(doc,
8✔
300
      do: Yex.Nif.xml_element_get_attribute(xml_element, cur_txn(xml_element), key)
8✔
301
    )
302
  end
303

304
  @doc """
305
  Returns a map of all attributes for this XML element.
306
  """
307
  @spec get_attributes(t) :: %{binary() => binary() | Yex.SharedType.t()}
308
  def get_attributes(%__MODULE__{doc: doc} = xml_element) do
309
    Doc.run_in_worker_process(doc,
16✔
310
      do: Yex.Nif.xml_element_get_attributes(xml_element, cur_txn(xml_element))
16✔
311
    )
312
  end
313

314
  @doc """
315
  The next sibling of this type. Is null if this is the last child of its parent.
316
  """
317
  @spec next_sibling(t) :: Yex.XmlElement.t() | Yex.XmlText.t() | nil
318
  def next_sibling(%__MODULE__{doc: doc} = xml_element) do
319
    Doc.run_in_worker_process(doc,
25✔
320
      do: Yex.Nif.xml_element_next_sibling(xml_element, cur_txn(xml_element))
25✔
321
    )
322
  end
323

324
  @doc """
325
  The previous sibling of this type. Is null if this is the first child of its parent.
326
  """
327
  @spec prev_sibling(t) :: Yex.XmlElement.t() | Yex.XmlText.t() | nil
328
  def prev_sibling(%__MODULE__{doc: doc} = xml_element) do
329
    Doc.run_in_worker_process(doc,
5✔
330
      do: Yex.Nif.xml_element_prev_sibling(xml_element, cur_txn(xml_element))
5✔
331
    )
332
  end
333

334
  @doc """
335
  The parent that holds this type. Is null if this xml is a top-level XML type.
336
  """
337
  @spec parent(t) :: Yex.XmlElement.t() | Yex.XmlFragment.t() | nil
338
  def parent(%__MODULE__{doc: doc} = xml_element) do
339
    Doc.run_in_worker_process(doc,
4✔
340
      do: Yex.Nif.xml_element_parent(xml_element, cur_txn(xml_element))
4✔
341
    )
342
  end
343

344
  @spec to_string(t) :: binary()
345
  def to_string(%__MODULE__{doc: doc} = xml_element) do
346
    Doc.run_in_worker_process(doc,
9✔
347
      do: Yex.Nif.xml_element_to_string(xml_element, cur_txn(xml_element))
9✔
348
    )
349
  end
350

351
  defp cur_txn(%{doc: %Yex.Doc{reference: doc_ref}}) do
352
    Process.get(doc_ref, nil)
337✔
353
  end
354

355
  @spec as_prelim(t) :: Yex.XmlElementPrelim.t()
356
  def as_prelim(%__MODULE__{doc: doc} = xml_element) do
357
    Doc.run_in_worker_process doc do
12✔
358
      c =
12✔
359
        children(xml_element)
360
        |> Enum.map(fn child -> Yex.Output.as_prelim(child) end)
10✔
361

362
      Yex.XmlElementPrelim.new(
12✔
363
        get_tag(xml_element),
364
        c,
365
        get_attributes(xml_element)
366
      )
367
    end
368
  end
369

370
  defimpl Yex.Output do
371
    def as_prelim(xml_element) do
372
      Yex.XmlElement.as_prelim(xml_element)
7✔
373
    end
374
  end
375

376
  defimpl Yex.Xml do
377
    defdelegate next_sibling(xml), to: Yex.XmlElement
24✔
378
    defdelegate prev_sibling(xml), to: Yex.XmlElement
4✔
379
    defdelegate parent(xml), to: Yex.XmlElement
1✔
380
    defdelegate to_string(xml), to: Yex.XmlElement
1✔
381
  end
382
end
383

384
defmodule Yex.XmlElementPrelim do
385
  @moduledoc """
386
  A preliminary xml element. It can be used to early initialize the contents of a XmlElement.
387

388
  ## Examples
389
      iex> doc = Yex.Doc.new()
390
      iex> xml = Yex.Doc.get_xml_fragment(doc, "xml")
391
      iex> Yex.XmlFragment.insert(xml, 0,  Yex.XmlElementPrelim.empty("div"))
392
      iex> Yex.XmlFragment.to_string(xml)
393
      "<div></div>"
394

395
  """
396
  defstruct [:tag, :attributes, :children]
397

398
  @type t :: %__MODULE__{
399
          tag: String.t(),
400
          attributes: %{String.t() => String.t() | Yex.PrelimType.t()},
401
          children: [Yex.XmlElementPrelim.t() | Yex.XmlTextPrelim.t()]
402
        }
403

404
  def new(tag, children, attributes \\ %{}) do
405
    %__MODULE__{
25✔
406
      tag: tag,
407
      attributes: attributes,
408
      children: Enum.to_list(children)
409
    }
410
  end
411

412
  def empty(tag) do
413
    %__MODULE__{
127✔
414
      tag: tag,
415
      attributes: %{},
416
      children: []
417
    }
418
  end
419
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