Skip to content

Commit e920fc7

Browse files
committed
SDK: add ! pipelining
This change provides part 1 of changes in support of pipelining API functions: where functions without the `!` suffix might return `{:error, error}`, those with the `!` will raise a `RuntimeError`. Note that the underlying implementation is essentially a copy of that found in [`Unsafe`](https://github.com/whitfin/unsafe/tree/main). Minor adjustments have been made to integrate more closely with Playwright, and match desired usage and execution patterns. An initial usage, for demonstration purposes, may be found at `BrowserContext.add_cookies/2`.
1 parent 26fed18 commit e920fc7

File tree

4 files changed

+190
-3
lines changed

4 files changed

+190
-3
lines changed

lib/playwright/browser_context.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ defmodule Playwright.BrowserContext do
249249
| `:secure` | `boolean()` | *(optional)* |
250250
| `:sameSite` | `binary()` | *(optional)* one of "Strict", "Lax", "None" |
251251
"""
252+
@pipe {:add_cookies, [:context, :cookies]}
252253
@spec add_cookies(t(), [cookie]) :: t()
253254
def add_cookies(context, cookies)
254255

lib/playwright/sdk/channel_owner.ex

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ defmodule Playwright.SDK.ChannelOwner do
152152
defmacro __using__(_args) do
153153
Module.register_attribute(__CALLER__.module, :properties, accumulate: true)
154154

155-
quote do
155+
quote(location: :keep) do
156+
use Playwright.SDK.Pipeline
156157
import Kernel, except: [@: 1]
157158
import unquote(__MODULE__), only: [@: 1]
158159
end
@@ -162,7 +163,7 @@ defmodule Playwright.SDK.ChannelOwner do
162163
Module.put_attribute(module, :properties, arg)
163164
doc = Keyword.get(options, :doc, false)
164165

165-
quote do
166+
quote(location: :keep) do
166167
@doc unquote(doc)
167168
@spec unquote(arg)(t()) :: term()
168169
def unquote(arg)(owner) do
@@ -194,7 +195,7 @@ defmodule Playwright.SDK.ChannelOwner do
194195
end
195196

196197
defmacro @expr do
197-
quote do
198+
quote(location: :keep) do
198199
Kernel.@(unquote(expr))
199200
end
200201
end

lib/playwright/sdk/pipeline.ex

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
defmodule Playwright.SDK.Pipeline do
2+
@moduledoc false
3+
4+
# NOTE: This module is essentially copy of `Unsafe.Generator`, with the
5+
# following minor modifications per Playwright style wishes:
6+
#
7+
# - Remap `@unsafe` as `@pipe`
8+
# - Embed `unwrap/1` and `unwrap/2` handlers
9+
# - Render customized `@doc` description
10+
#
11+
# See [`Unsafe`](https://hexdocs.pm/unsafe/Unsafe.html) for the original.
12+
defmodule Compiler do
13+
@moduledoc false
14+
15+
# definitions for multiple arities
16+
@type arities :: arity | [arity]
17+
18+
# definitions for function definition bindings
19+
@type binding :: {atom, arities} | {atom, arities, handler}
20+
21+
# definitions for handler functions
22+
@type handler :: atom | {atom, atom}
23+
24+
@doc false
25+
@spec compile!(Macro.Env.t(), binding | [binding], Keyword.t()) :: Macro.t()
26+
def compile!(env, bindings, options) when is_list(bindings),
27+
do: Enum.map(bindings, &compile!(env, &1, options))
28+
29+
# This will fire if the provided binding does not contain a
30+
# valid handler. If this is the case, the handler option will
31+
# be pulled from the binding options and passed through as the
32+
# handler to use for the binding (without any validation).
33+
def compile!(env, {name, arity}, options),
34+
do: compile!(env, {name, arity, options[:handler]}, options)
35+
36+
# This definition fires when the provided binding includes a list of
37+
# arities and unpacks them into a list of bindings per arity. It then
38+
# just passes the results through to the same compile!/3 function
39+
# in order to make use of the same processing as any other bindings.
40+
def compile!(env, {name, [head | _] = arity, handler}, options)
41+
when is_integer(head) do
42+
arity
43+
|> Enum.map(&{name, &1, handler})
44+
|> Enum.map(&compile!(env, &1, options))
45+
end
46+
47+
# This is the main definition which will compile a binding into a new
48+
# unsafe function handle, ready to be included in a module at compile
49+
# time. Arguments are generated based on the arity list provided and
50+
# passed to the main function reference, before being passed through
51+
# a validated handler and being returned from the unsafe function.
52+
def compile!(env, {name, arity, handler}, options) do
53+
# determine whether we have arguments or arity
54+
{enum, length, generator} =
55+
if is_list(arity) do
56+
# use an accepted arguments list with provided name definitions
57+
{arity, length(arity), &Macro.var(&1, env.module)}
58+
else
59+
# create an argument list based on the arity; [ arg0, arg1, etc... ]
60+
{0..(arity && arity - 1), arity, &Macro.var(:"arg#{&1}", env.module)}
61+
end
62+
63+
# generate the parameters used to define the proxy
64+
params = Enum.map(enum, generator)
65+
66+
# create a definition for the proxy; apply(env.module, name, params)
67+
result =
68+
quote do: apply(unquote(env.module), unquote(name), unquote(params))
69+
70+
# generate the handler definition
71+
handle =
72+
case handler do
73+
# private function names as atoms
74+
func when is_atom(func) and not is_nil(func) ->
75+
# can be through of as func(result)
76+
quote do: unquote(func)(unquote(result))
77+
78+
# public functions
79+
{mod, func} ->
80+
# can be thought of as apply(mod, func, [ result ])
81+
quote do: apply(unquote(mod), unquote(func), [unquote(result)])
82+
83+
# bad definitions
84+
_fail ->
85+
raise CompileError,
86+
description: "Invalid handler definition for #{name}/#{length}",
87+
file: env.file,
88+
line: env.line
89+
end
90+
91+
# generate documentation tags
92+
ex_docs =
93+
if options[:docs] do
94+
# use a forwarding documentation message based on the function definition
95+
quote do:
96+
@doc(
97+
"Pipeline proxy for `#{unquote(name)}/#{unquote(length)}`. Instead of returning `{:error, error}`, `#{unquote(name)}/#{unquote(length)}!` will raise a `RuntimeError`."
98+
)
99+
else
100+
# disable documentation
101+
quote do: @doc(false)
102+
end
103+
104+
# compile
105+
quote do
106+
# unpack the docs
107+
unquote(ex_docs)
108+
109+
# add the function definition and embed the handle inside
110+
def unquote(:"#{name}!")(unquote_splicing(params)) do
111+
unquote(handle)
112+
end
113+
end
114+
end
115+
116+
# Finally, if this definition fires, the provided definition
117+
# for the binding was totally invalid and should cause errors.
118+
def compile!(env, _invalid, _options),
119+
do:
120+
raise(CompileError,
121+
description: "Invalid function reference provided",
122+
file: env.file,
123+
line: env.line
124+
)
125+
end
126+
127+
defmodule Generator do
128+
@moduledoc false
129+
alias Compiler
130+
alias Generator
131+
132+
# Hook for the `use` syntax, which will automatically configure
133+
# the calling module to use the attributes required for generation.
134+
defmacro __using__(options) do
135+
quote location: :keep do
136+
@before_compile Generator
137+
@pipeline_options unquote(options)
138+
139+
Module.register_attribute(__MODULE__, :pipe, accumulate: true)
140+
end
141+
end
142+
143+
# The compiler hook which will invoke the main compilation phase
144+
# found in `Unsafe.Compiler.compile/3` to compile the definitions.
145+
defmacro __before_compile__(%{module: module} = env) do
146+
binding = Module.get_attribute(module, :pipe)
147+
148+
options =
149+
module
150+
|> Module.get_attribute(:pipeline_options)
151+
|> Kernel.||([])
152+
153+
Compiler.compile!(env, binding, options)
154+
end
155+
end
156+
157+
defmacro __using__(_) do
158+
quote location: :keep do
159+
use Playwright.SDK.Pipeline.Generator, docs: true, handler: :unwrap
160+
161+
defp unwrap({:error, error}) do
162+
raise(RuntimeError, message: error.message)
163+
end
164+
165+
defp unwrap(result) do
166+
result
167+
end
168+
end
169+
end
170+
end

test/api/browser_context/add_cookies_test.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,19 @@ defmodule Playwright.BrowserContext.AddCookiesTest do
4040
# test_should_set_cookies_for_a_frame
4141
# test_should_not_block_third_party_cookies
4242
end
43+
44+
describe "BrowserContext.add_cookies!/2" do
45+
test "on success, returns 'subject", %{assets: assets, page: page} do
46+
context = Page.owned_context(page)
47+
cookies = [%{url: assets.empty, name: "password", value: "123456"}]
48+
assert %BrowserContext{} = BrowserContext.add_cookies(context, cookies)
49+
end
50+
51+
test "on failure, raises `RuntimeError`", %{page: page} do
52+
assert_raise RuntimeError, "cookies[0].name: expected string, got undefined", fn ->
53+
context = Page.owned_context(page)
54+
BrowserContext.add_cookies!(context, [%{bogus: "cookie"}])
55+
end
56+
end
57+
end
4358
end

0 commit comments

Comments
 (0)