browserctl 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/browserctl/errors.rb +6 -0
- data/lib/browserctl/flow.rb +194 -0
- data/lib/browserctl/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff8250901d49ad1038c686f51d303f2139fe673c708746804cdb59e1239890fe
|
|
4
|
+
data.tar.gz: b47c1e8dbf9093ffe43001d8cacf02c490c887fb5e58336449f1b7aedebfda0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e11c9a8300e7ec744e70ad5f33b645c9afc59e4e5212696045e899f4f2da017d1e4589f750db65b004d8e8208092d6a031724474a2a8feb48f249ed1daa83262
|
|
7
|
+
data.tar.gz: 1a48cb05e7d92ab6dfd2de777abd0d8904a30b27c8d03fdaee2c00d1b5b734a9f8499be90ea30db686022a5c0d9ad7532ea1c1f148f0e1047a0583a954048dd8
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,13 @@ All notable changes to this project will be documented in this file.
|
|
|
10
10
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
11
11
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
12
12
|
|
|
13
|
+
## [0.10.0](https://github.com/patrick204nqh/browserctl/compare/v0.9.0...v0.10.0) (2026-05-09)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* Browserctl::Flow class + DSL ([#82](https://github.com/patrick204nqh/browserctl/issues/82)) ([93282ed](https://github.com/patrick204nqh/browserctl/commit/93282edfc1358a371377c9ffbedf7cf1f6e7f567))
|
|
19
|
+
|
|
13
20
|
## [0.9.0](https://github.com/patrick204nqh/browserctl/compare/v0.8.4...v0.9.0) (2026-05-09)
|
|
14
21
|
|
|
15
22
|
|
data/lib/browserctl/errors.rb
CHANGED
|
@@ -27,4 +27,10 @@ module Browserctl
|
|
|
27
27
|
|
|
28
28
|
class WorkflowError < Error; def self.default_code = "workflow_error" end
|
|
29
29
|
class SecretResolverError < WorkflowError; def self.default_code = "secret_resolver_error" end
|
|
30
|
+
|
|
31
|
+
class FlowError < WorkflowError; def self.default_code = "flow_error" end
|
|
32
|
+
class FlowParamError < FlowError; def self.default_code = "flow_param_error" end
|
|
33
|
+
class FlowPreconditionError < FlowError; def self.default_code = "flow_precondition_failed" end
|
|
34
|
+
class FlowStepError < FlowError; def self.default_code = "flow_step_failed" end
|
|
35
|
+
class FlowPostconditionError < FlowError; def self.default_code = "flow_postcondition_failed" end
|
|
30
36
|
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "secret_resolvers"
|
|
6
|
+
|
|
7
|
+
module Browserctl
|
|
8
|
+
FlowParamDef = Struct.new(:name, :required, :secret, :default, :secret_ref, keyword_init: true)
|
|
9
|
+
FlowStepDef = Struct.new(:label, :block, :retry_count, :timeout, keyword_init: true)
|
|
10
|
+
FlowConditionDef = Struct.new(:kind, :label, :block, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
class FlowContext
|
|
13
|
+
attr_reader :page, :client, :params
|
|
14
|
+
|
|
15
|
+
def initialize(page:, params:, client: nil)
|
|
16
|
+
@page = page
|
|
17
|
+
@client = client
|
|
18
|
+
@params = params
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def method_missing(name, *args)
|
|
22
|
+
return @params[name] if args.empty? && @params.key?(name)
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def respond_to_missing?(name, include_private = false)
|
|
28
|
+
@params.key?(name) || super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Flow
|
|
33
|
+
SEMVER_RE = /\A\d+\.\d+\.\d+\z/
|
|
34
|
+
|
|
35
|
+
attr_reader :name,
|
|
36
|
+
:version_string,
|
|
37
|
+
:description,
|
|
38
|
+
:param_defs,
|
|
39
|
+
:steps,
|
|
40
|
+
:preconditions,
|
|
41
|
+
:postconditions,
|
|
42
|
+
:produces_state_block,
|
|
43
|
+
:min_browserctl_version
|
|
44
|
+
|
|
45
|
+
def initialize(name)
|
|
46
|
+
@name = name.to_s
|
|
47
|
+
@version_string = "0.0.0"
|
|
48
|
+
@description = nil
|
|
49
|
+
@param_defs = {}
|
|
50
|
+
@steps = []
|
|
51
|
+
@preconditions = []
|
|
52
|
+
@postconditions = []
|
|
53
|
+
@produces_state_block = nil
|
|
54
|
+
@min_browserctl_version = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def version(value)
|
|
58
|
+
validate_semver!(value, label: "version")
|
|
59
|
+
@version_string = value.to_s
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def requires_browserctl(value)
|
|
63
|
+
validate_semver!(value, label: "requires_browserctl")
|
|
64
|
+
@min_browserctl_version = value.to_s
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def desc(text)
|
|
68
|
+
@description = text.to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def param(name, required: false, secret: false, default: nil, secret_ref: nil)
|
|
72
|
+
secret = true if secret_ref
|
|
73
|
+
@param_defs[name] = FlowParamDef.new(
|
|
74
|
+
name: name,
|
|
75
|
+
required: required,
|
|
76
|
+
secret: secret,
|
|
77
|
+
default: default,
|
|
78
|
+
secret_ref: secret_ref
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def step(label, retry_count: 0, timeout: nil, &block)
|
|
83
|
+
raise ArgumentError, "flow step '#{label}' requires a block" unless block
|
|
84
|
+
|
|
85
|
+
@steps << FlowStepDef.new(label: label, block: block, retry_count: retry_count, timeout: timeout)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def precondition(label = "precondition", &block)
|
|
89
|
+
raise ArgumentError, "precondition '#{label}' requires a block" unless block
|
|
90
|
+
|
|
91
|
+
@preconditions << FlowConditionDef.new(kind: :precondition, label: label, block: block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def postcondition(label = "postcondition", &block)
|
|
95
|
+
raise ArgumentError, "postcondition '#{label}' requires a block" unless block
|
|
96
|
+
|
|
97
|
+
@postconditions << FlowConditionDef.new(kind: :postcondition, label: label, block: block)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def produces_state(&block)
|
|
101
|
+
raise ArgumentError, "produces_state requires a block" unless block
|
|
102
|
+
|
|
103
|
+
@produces_state_block = block
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def run(page: nil, client: nil, **params)
|
|
107
|
+
ctx = FlowContext.new(page: page, client: client, params: resolve_params(params))
|
|
108
|
+
|
|
109
|
+
run_conditions(ctx, @preconditions, error_class: FlowPreconditionError)
|
|
110
|
+
run_steps(ctx)
|
|
111
|
+
run_conditions(ctx, @postconditions, error_class: FlowPostconditionError)
|
|
112
|
+
|
|
113
|
+
produce_state(ctx)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def validate_semver!(value, label:)
|
|
119
|
+
return if value.to_s.match?(SEMVER_RE)
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "#{label} must be MAJOR.MINOR.PATCH (got #{value.inspect})"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def resolve_params(provided)
|
|
125
|
+
@param_defs.each_with_object({}) do |(name, defn), out|
|
|
126
|
+
val = if defn.secret_ref
|
|
127
|
+
SecretResolverRegistry.resolve(defn.secret_ref)
|
|
128
|
+
elsif provided.key?(name)
|
|
129
|
+
provided[name]
|
|
130
|
+
else
|
|
131
|
+
defn.default
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
raise FlowParamError, "flow '#{@name}' requires param '#{name}'" if defn.required && val.nil?
|
|
135
|
+
|
|
136
|
+
out[name] = val
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def run_conditions(ctx, conditions, error_class:)
|
|
141
|
+
conditions.each do |cond|
|
|
142
|
+
result = ctx.instance_exec(&cond.block)
|
|
143
|
+
next if result
|
|
144
|
+
|
|
145
|
+
raise error_class,
|
|
146
|
+
"flow '#{@name}' #{cond.kind} '#{cond.label}' returned #{result.inspect}"
|
|
147
|
+
rescue FlowError
|
|
148
|
+
raise
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
raise error_class,
|
|
151
|
+
"flow '#{@name}' #{cond.kind} '#{cond.label}' raised: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def run_steps(ctx)
|
|
156
|
+
@steps.each { |defn| run_step(ctx, defn) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def run_step(ctx, defn)
|
|
160
|
+
last_error = nil
|
|
161
|
+
(defn.retry_count + 1).times do
|
|
162
|
+
execute_step_block(ctx, defn)
|
|
163
|
+
return
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
last_error = e
|
|
166
|
+
end
|
|
167
|
+
raise FlowStepError,
|
|
168
|
+
"flow '#{@name}' step '#{defn.label}' failed: #{last_error.message}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def execute_step_block(ctx, defn)
|
|
172
|
+
if defn.timeout
|
|
173
|
+
::Timeout.timeout(defn.timeout) { ctx.instance_exec(&defn.block) }
|
|
174
|
+
else
|
|
175
|
+
ctx.instance_exec(&defn.block)
|
|
176
|
+
end
|
|
177
|
+
rescue ::Timeout::Error
|
|
178
|
+
raise FlowStepError,
|
|
179
|
+
"flow '#{@name}' step '#{defn.label}' timed out after #{defn.timeout}s"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def produce_state(ctx)
|
|
183
|
+
return nil unless @produces_state_block
|
|
184
|
+
|
|
185
|
+
ctx.instance_exec(&@produces_state_block)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.flow(name, &block)
|
|
190
|
+
raise ArgumentError, "Browserctl.flow requires a block" unless block
|
|
191
|
+
|
|
192
|
+
Flow.new(name).tap { |f| f.instance_exec(&block) }
|
|
193
|
+
end
|
|
194
|
+
end
|
data/lib/browserctl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browserctl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -190,6 +190,7 @@ files:
|
|
|
190
190
|
- lib/browserctl/driver/cdp.rb
|
|
191
191
|
- lib/browserctl/driver/cdp_page.rb
|
|
192
192
|
- lib/browserctl/errors.rb
|
|
193
|
+
- lib/browserctl/flow.rb
|
|
193
194
|
- lib/browserctl/logger.rb
|
|
194
195
|
- lib/browserctl/policy.rb
|
|
195
196
|
- lib/browserctl/recording.rb
|