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

christhekeele / matcha / c6283b1b9bb7327327b0d71ceeefc0a6376715f6

21 Nov 2023 06:40PM UTC coverage: 63.884% (+0.1%) from 63.782%
c6283b1b9bb7327327b0d71ceeefc0a6376715f6

push

github

christhekeele
Change 'source' accessor to 'raw'; emphasize 'raw' terminology in docs.

3 of 18 new or added lines in 7 files covered. (16.67%)

2 existing lines in 2 files now uncovered.

398 of 623 relevant lines covered (63.88%)

216.91 hits per line

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

50.0
/lib/matcha/context.ex
1
defmodule Matcha.Context do
2
  @moduledoc """
3
  Different types of match spec are intended to be used for different purposes,
4
  and support different instructions in their bodies for different use-cases.
5

6
  The modules implementing the `Matcha.Context` behaviour define the different types of `Matcha.Spec`,
7
  provide documentation for what specialized instructions that type supports, and are used during
8
  Elixir-to-match spec conversion as a concrete function definition to use when expanding instructions
9
  (since most of these specialized instructions do not exist anywhere as an actual functions,
10
  this lets the Elixir compiler complain about invalid instructions as `UndefinedFunctionError`s).
11

12
  ### Predefined contexts
13

14
  Currently there are three applications of match specs supported:
15

16
    - `:filter_map`:
17

18
        Matchspecs intended to be used to filter/map over an in-memory list in an optimized fashion.
19
        These types of match spec reference the `Matcha.Context.FilterMap` module.
20

21
    - `:match`:
22

23
        Matchspecs intended to be used to match over an in-memory list in an optimized fashion.
24
        These types of match spec reference the `Matcha.Context.Match` module.
25

26
    - `:table`:
27

28
        Matchspecs intended to be used to efficiently select data from BEAM VM "table"
29
        tools, such as `:ets`, `:dets`, and `:mnesia`, and massage the values returned.
30
        These types of match spec reference the `Matcha.Context.Table` module.
31

32
    - `:trace`:
33

34
        Matchspecs intended to be used to instruct tracing utilities such as
35
        `:dbg` and `:recon_trace` exactly what function calls with what arguments to trace,
36
        and allows invoking special trace command instructions in response.
37
        These types of match spec reference the `Matcha.Context.Trace` module.
38

39
  ### Custom contexts
40

41
  The context mechanism is technically extensible: any module can implement the `Matcha.Context`
42
  behaviour, define the callbacks, and list public no-op functions to allow their usage in
43
  specs compiled with that context (via `Matcha.spec(CustomContext) do...`).
44

45
  In practice there is little point in defining a custom context:
46
  the supported use-cases for match specs are tightly coupled to the Erlang language,
47
  and `Matcha` covers all of them with its provided contexts, which should be sufficient for any application.
48
  The module+behaviour+callback implementation used in `Matcha` is less about offering extensibility,
49
  but instead used to simplify special-casing in `Matcha.Spec` function implementations,
50
  raise Elixir-flavored errors when an invalid instruction is used in the different types of spec,
51
  and provide a place to document what they do when invoked.
52

53
  """
54

55
  defmacro __using__(_opts \\ []) do
56
    quote do
57
      @behaviour unquote(__MODULE__)
58
    end
59
  end
60

61
  alias Matcha.Error
62
  alias Matcha.Source
63

64
  alias Matcha.Spec
65

66
  @type t :: module()
67

68
  @core_context_aliases [
69
    filter_map: Matcha.Context.FilterMap,
70
    match: Matcha.Context.Match,
71
    table: Matcha.Context.Table,
72
    trace: Matcha.Context.Trace
73
  ]
74

75
  @spec __core_context_aliases__() :: Keyword.t()
76
  @doc """
77
  Maps the shortcut references to the core Matcha context modules.
78

79
  This describes which shortcuts users may write, for example in `Matcha.spec(:some_shortcut)` instead of
80
  the fully qualified module `Matcha.spec(Matcha.Context.SomeContext)`.
81
  """
82
  def __core_context_aliases__(), do: @core_context_aliases
×
83

84
  @doc """
85
  Which primitive Erlang context this context module wraps.
86
  """
87
  @callback __erl_spec_type__() :: Source.type()
88

89
  @doc """
90
  A default value to use when executing match specs in this context.
91

92
  This function is used to provide `Matcha.Source.test/3` with a target value to test against,
93
  in situations where it is being used to simply validate the match spec itself,
94
  but we do not acutally care if the input matches the spec.
95

96
  This value, when passed to this context's `c:Matcha.Context.__valid_match_target__/1` callback,
97
  must produce a `true` value.
98
  """
99
  @callback __default_match_target__() :: any
100

101
  @doc """
102
  A validator that runs before executing a match spec against a `target` in this context.
103

104
  This validator is run before any match specs are executed on inputs to `Matcha.Source.test/3`,
105
  and all elements of the enumerable input to `Matcha.Source.run/2`.
106

107
  If this function returns false, the match spec will not be executed, instead
108
  returning a `t:Matcha.Error.error_problem` with a `t:Matcha.Error.message`
109
  generated by the `c:Matcha.Context.__invalid_match_target_error_message__/1` callback.
110
  """
111
  @callback __valid_match_target__(match_target :: any) :: boolean()
112

113
  @doc """
114
  Describes an issue with a test target.
115

116
  Invoked to generate a `t:Matcha.Error.message` when `c:Matcha.Context.__valid_match_target__/1` fails.
117
  """
118
  @callback __invalid_match_target_error_message__(match_target :: any) :: binary
119

120
  @doc """
121
  Allows this context module to modify match specs before their execution.
122

123
  This hook is the main entrypoint for creating custom contexts,
124
  allowing them to augment the match spec with new behaviour when executed in this context.
125

126
  Care must be taken to handle the results of the modified match spec after execution correctly,
127
  before they are returned to the caller. This should be implemented in the callbacks:
128

129
  - `c:__transform_erl_run_results__/1`
130
  - `c:__transform_erl_test_result__/1`
131
  - `c:__emit_erl_test_result__/1`
132
  """
133
  @callback __prepare_source__(source :: Source.uncompiled()) ::
134
              {:ok, new_source :: Source.uncompiled()} | {:error, Error.problems()}
135
  @doc """
136
  Transforms the result of a spec match just after calling `:erlang.match_spec_test/3`.
137

138
  You can think of this as an opportunity to "undo" any modifications to the user's
139
  provided matchspec made in `c:__prepare_source__/1`.
140

141
  Must return `{:ok, result}` to indicate that the returned value is valid; otherwise
142
  return `{:error, problems}` to raise an exception.
143
  """
144
  @callback __transform_erl_test_result__(result :: any) ::
145
              {:ok, result :: any} | {:error, Error.problems()}
146

147
  @doc """
148
  Transforms the result of a spec match just after calling `:ets.match_spec_run/2`.
149

150
  You can think of this as an opportunity to "undo" any modifications to the user's
151
  provided matchspec made in `c:__prepare_source__/1`.
152

153
  Must return `{:ok, result}` to indicate that the returned value is valid; otherwise
154
  return `{:error, problems}` to raise an exception.
155
  """
156
  @callback __transform_erl_run_results__(results :: [any]) ::
157
              {:ok, results :: [any]} | {:error, Error.problems()}
158

159
  @doc """
160
  Decides if the result of a spec match should be part of the result set.
161

162
  This callback runs just after calls to `c:__transform_erl_test_result__/1` or `c:__transform_erl_test_result__/1`.
163

164
  Must return `{:emit, result}` to include the transformed result of a spec match, when executing it
165
  against in-memory data (as opposed to tracing or :ets) for validation or debugging purposes.
166
  Otherwise, returning `:no_emit` will hide the result.
167
  """
168
  @callback __emit_erl_test_result__(result :: any) :: {:emit, new_result :: any} | :no_emit
169

170
  @doc """
171
  Determines whether or not specs in this context can be compiled.
172
  """
173
  @spec supports_compilation?(t) :: boolean
174
  def supports_compilation?(context) do
175
    context.__erl_spec_type__() == :table
365✔
176
  end
177

178
  @doc """
179
  Determines whether or not specs in this context can used in tracing.
180
  """
181
  @spec supports_tracing?(t) :: boolean
182
  def supports_tracing?(context) do
183
    context.__erl_spec_type__() == :trace
1✔
184
  end
185

186
  @doc """
187
  Resolves shortcut references to the core Matcha context modules.
188

189
  This allows users to write, for example, `Matcha.spec(:trace)` instead of
190
  the fully qualified module `Matcha.spec(Matcha.Context.Trace)`.
191
  """
192
  @spec resolve(atom() | t) :: t | no_return
193

194
  for {alias, context} <- @core_context_aliases do
195
    def resolve(unquote(alias)), do: unquote(context)
21✔
196
  end
197

198
  def resolve(context) when is_atom(context) do
523✔
199
    context.__erl_spec_type__()
523✔
200
  rescue
201
    UndefinedFunctionError ->
202
      reraise ArgumentError,
1✔
203
              [
204
                message:
205
                  "`#{inspect(context)}` is not one of: " <>
206
                    (Keyword.keys(@core_context_aliases)
207
                     |> Enum.map_join(", ", &"`#{inspect(&1)}`")) <>
4✔
208
                    " or a module that implements `Matcha.Context`"
209
              ],
210
              __STACKTRACE__
211
  else
212
    _ -> context
522✔
213
  end
214

215
  def resolve(context) do
216
    raise ArgumentError,
1✔
217
      message:
218
        "`#{inspect(context)}` is not one of: " <>
219
          (Keyword.keys(@core_context_aliases)
220
           |> Enum.map_join(", ", &"`#{inspect(&1)}`")) <>
4✔
221
          " or a module that implements `Matcha.Context`"
222
  end
223

224
  @spec run(Matcha.Spec.t(), Enumerable.t()) ::
225
          {:ok, list(any)} | {:error, Error.problems()}
226
  @doc """
227
  Runs a `spec` against an `enumerable`.
228

229
  This is a key function that ensures the input `spec` and results
230
  are passed through the callbacks of a `#{inspect(__MODULE__)}`.
231

232
  Returns either `{:ok, results}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
233
  """
234
  def run(%Spec{context: context} = spec, enumerable) do
235
    case context.__prepare_source__(Spec.raw(spec)) do
365✔
236
      {:ok, source} ->
237
        match_targets = Enum.to_list(enumerable)
365✔
238
        # TODO: validate targets pre-run
239
        # spec.context.__valid_match_target__(match_target)
240

241
        results =
365✔
242
          if supports_compilation?(context) do
243
            source
244
            |> Source.compile()
245
            |> Source.run(match_targets)
365✔
246
          else
247
            do_run_without_compilation(match_targets, spec, source)
×
248
          end
249

250
        spec.context.__transform_erl_run_results__(results)
365✔
251

252
      {:error, problems} ->
×
253
        {:error, problems}
254
    end
255
  end
256

257
  defp do_run_without_compilation(match_targets, spec, source) do
258
    match_targets
259
    |> Enum.reduce([], fn match_target, results ->
260
      case do_test(source, spec.context, match_target) do
×
261
        {:ok, result} ->
262
          case spec.context.__emit_erl_test_result__(result) do
×
263
            {:emit, result} ->
×
264
              [result | results]
265

266
            :no_emit ->
×
267
              {[], spec}
268
          end
269

270
        {:error, problems} ->
271
          raise Spec.Error,
×
272
            source: spec,
273
            details: "when running match spec",
274
            problems: problems
275
      end
276
    end)
277
    |> :lists.reverse()
×
278
  end
279

280
  @doc """
281
  Creates a lazy `Stream` that yields the results of running the `spec` against the provided `enumberable`.
282

283
  This is a key function that ensures the input `spec` and results
284
  are passed through the callbacks of a `#{inspect(__MODULE__)}`.
285

286
  Returns either `{:ok, stream}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
287
  """
288
  @spec stream(Matcha.Spec.t(), Enumerable.t()) ::
289
          {:ok, Enumerable.t()} | {:error, Error.problems()}
290
  def stream(%Spec{context: context} = spec, enumerable) do
NEW
291
    case context.__prepare_source__(Spec.raw(spec)) do
×
292
      {:ok, source} ->
293
        Stream.transform(enumerable, {spec, source}, fn match_target, {spec, source} ->
×
294
          # TODO: validate targets midstream
295
          # spec.context.__valid_match_target__(match_target)
296
          do_stream_test(match_target, spec, source)
×
297
        end)
298

299
      {:error, problems} ->
×
300
        {:error, problems}
301
    end
302
  end
303

304
  defp do_stream_test(match_target, spec, source) do
305
    case do_test(source, spec.context, match_target) do
×
306
      {:ok, result} ->
307
        case spec.context.__emit_erl_test_result__(result) do
×
308
          {:emit, result} ->
309
            case spec.context.__transform_erl_run_results__([result]) do
×
310
              {:ok, results} ->
×
311
                {:ok, results}
312

313
              {:error, problems} ->
314
                raise Spec.Error,
×
315
                  source: spec,
316
                  details: "when streaming match spec",
317
                  problems: problems
318
            end
319

320
          :no_emit ->
×
321
            {[], spec}
322
        end
323

324
      {:error, problems} ->
325
        raise Spec.Error,
×
326
          source: spec,
327
          details: "when streaming match spec",
328
          problems: problems
329
    end
330
  end
331

332
  @spec test(Spec.t()) ::
333
          {:ok, any} | {:error, Error.problems()}
334
  @doc """
335
  Tests that the provided `spec` in  its `Matcha.Context` is valid.
336

337
  Invokes `c:__default_match_target__/0` and passes it into `:erlang.match_spec_test/3`.
338

339
  Returns either `{:ok, stream}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
340
  """
341
  def test(%Spec{context: context} = spec) do
342
    test(spec, context.__default_match_target__())
720✔
343
  end
344

345
  @spec test(Spec.t(), Source.match_target()) ::
346
          {:ok, any} | {:error, Error.problems()}
347
  @doc """
348
  Tests that the provided `spec` in its `Matcha.Context` correctly matches a provided `match_target`.
349

350
  Passes the provided `match_target` into `:erlang.match_spec_test/3`.
351

352
  Returns either `{:ok, stream}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
353
  """
354
  def test(%Spec{context: context} = spec, match_target) do
355
    case context.__prepare_source__(Spec.raw(spec)) do
1,301✔
356
      {:ok, source} ->
357
        if context.__valid_match_target__(match_target) do
1,301✔
358
          do_test(source, context, match_target)
1,301✔
359
        else
360
          {:error,
361
           [
362
             error: context.__invalid_match_target_error_message__(match_target)
363
           ]}
364
        end
365

366
      {:error, problems} ->
×
367
        {:error, problems}
368
    end
369
  end
370

371
  defp do_test(source, context, match_target) do
372
    source
373
    |> Source.test(context.__erl_spec_type__(), match_target)
1,301✔
374
    |> context.__transform_erl_test_result__()
1,301✔
375
  end
376
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