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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e27f4db200c9e26aaad474df3f55c1a7c91d602724d900d462a969ce4506ba9f
4
- data.tar.gz: fba2acf3be3adf3a3a5ca9b42026abfcbfbeea7a8f8d48e51cd455b710fb403c
3
+ metadata.gz: ff8250901d49ad1038c686f51d303f2139fe673c708746804cdb59e1239890fe
4
+ data.tar.gz: b47c1e8dbf9093ffe43001d8cacf02c490c887fb5e58336449f1b7aedebfda0b
5
5
  SHA512:
6
- metadata.gz: 8ac208ae09f276efbf624ed297ef725d5144ec9c13624127f7f949491b0156f7a712ee22dc45cbe218addfe88227e7aa9676feaa105ef1813743b30ac3d5cd29
7
- data.tar.gz: e5de1ed64d9d88e69c59a6de15bcfefaeb17b28cb578a8f6cddc2f5c97a8ee6d752c92efb6eff5d533d5923bfdcf6f29464a9f8bf06400953f6f874681c75689
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Browserctl
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
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.9.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