bidi2pdf 0.1.15 → 0.1.17

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +20 -0
  3. data/CHANGELOG.md +46 -2
  4. data/README.md +233 -11
  5. data/docker/Dockerfile +11 -0
  6. data/docker/Dockerfile.chromedriver +7 -0
  7. data/docker/Dockerfile.slim +12 -1
  8. data/lib/bidi2pdf/bidi/buffered_web_socket_client.rb +9 -0
  9. data/lib/bidi2pdf/bidi/session.rb +5 -1
  10. data/lib/bidi2pdf/cli/json_output.rb +30 -0
  11. data/lib/bidi2pdf/cli.rb +559 -17
  12. data/lib/bidi2pdf/diagnose.rb +119 -0
  13. data/lib/bidi2pdf/error_codes.rb +59 -0
  14. data/lib/bidi2pdf/exit_codes.rb +44 -0
  15. data/lib/bidi2pdf/launcher.rb +23 -0
  16. data/lib/bidi2pdf/manifest.rb +65 -0
  17. data/lib/bidi2pdf/notifications/json_subscriber.rb +78 -0
  18. data/lib/bidi2pdf/pdf_inspection.rb +62 -0
  19. data/lib/bidi2pdf/recipe/loader.rb +44 -0
  20. data/lib/bidi2pdf/recipe/runner.rb +229 -0
  21. data/lib/bidi2pdf/recipe/schema_shape.rb +228 -0
  22. data/lib/bidi2pdf/recipe/validator.rb +138 -0
  23. data/lib/bidi2pdf/recipe.rb +83 -0
  24. data/lib/bidi2pdf/result.rb +67 -0
  25. data/lib/bidi2pdf/result_collector.rb +150 -0
  26. data/lib/bidi2pdf/schema.rb +382 -0
  27. data/lib/bidi2pdf/session_registry.rb +97 -0
  28. data/lib/bidi2pdf/session_runner.rb +42 -0
  29. data/lib/bidi2pdf/session_sweeper.rb +64 -0
  30. data/lib/bidi2pdf/session_warmer.rb +51 -1
  31. data/lib/bidi2pdf/version.rb +1 -1
  32. data/lib/bidi2pdf.rb +92 -8
  33. data/sig/bidi2pdf/bidi/buffered_web_socket_client.rbs +7 -0
  34. data/sig/bidi2pdf/bidi/session.rbs +6 -0
  35. data/sig/bidi2pdf/cli/json_output.rbs +19 -0
  36. data/sig/bidi2pdf/cli.rbs +112 -2
  37. data/sig/bidi2pdf/diagnose.rbs +30 -0
  38. data/sig/bidi2pdf/error_codes.rbs +21 -0
  39. data/sig/bidi2pdf/exit_codes.rbs +21 -0
  40. data/sig/bidi2pdf/launcher.rbs +9 -0
  41. data/sig/bidi2pdf/manifest.rbs +38 -0
  42. data/sig/bidi2pdf/notifications/json_subscriber.rbs +45 -0
  43. data/sig/bidi2pdf/pdf_inspection.rbs +33 -0
  44. data/sig/bidi2pdf/recipe/loader.rbs +20 -0
  45. data/sig/bidi2pdf/recipe/runner.rbs +92 -0
  46. data/sig/bidi2pdf/recipe/schema_shape.rbs +85 -0
  47. data/sig/bidi2pdf/recipe/validator.rbs +54 -0
  48. data/sig/bidi2pdf/recipe.rbs +72 -0
  49. data/sig/bidi2pdf/result.rbs +71 -0
  50. data/sig/bidi2pdf/result_collector.rbs +106 -0
  51. data/sig/bidi2pdf/schema.rbs +45 -0
  52. data/sig/bidi2pdf/session_registry.rbs +44 -0
  53. data/sig/bidi2pdf/session_runner.rbs +17 -0
  54. data/sig/bidi2pdf/session_sweeper.rbs +36 -0
  55. data/sig/bidi2pdf/session_warmer.rbs +33 -0
  56. data/sig/bidi2pdf/version.rbs +1 -1
  57. data/sig/bidi2pdf.rbs +66 -0
  58. metadata +43 -3
@@ -0,0 +1,21 @@
1
+ module Bidi2pdf
2
+ # Maps an exception to the stable, machine-readable code used by --json/--json-stream/manifests
3
+ # and recipe results. Derived from the existing Bidi2pdf::Error hierarchy rather than duplicated
4
+ # beside it: a new failure mode gets a new subclass in lib/bidi2pdf.rb and a row here, never a
5
+ # bare string minted ad hoc.
6
+ #
7
+ # MAPPING is ordered most-specific-subclass first - #for walks it in order and returns the first
8
+ # match, so a subclass must appear before any of its ancestors.
9
+ module ErrorCodes
10
+ MAPPING: ::Hash[untyped, "MISSING_INPUT" | "MULTIPLE_INPUT_SOURCES" | "EMPTY_INPUT" | "INVALID_CONFIG" | "INVALID_PRINT_OPTION" | "INVALID_RECIPE" | "PDF_INSPECTION_UNAVAILABLE" | "BROWSER_LAUNCH_FAILED" | "COMMAND_TIMEOUT" | "BROWSER_DISCONNECTED" | "NAVIGATION_TIMEOUT" | "NAVIGATION_AUTH" | "NAVIGATION_NOT_FOUND" | "DNS_ERROR" | "NAVIGATION_FAILED" | "SELECTOR_NOT_FOUND" | "PAGE_NOT_AS_EXPECTED" | "SCRIPT_ERROR" | "STYLE_ERROR" | "PDF_GENERATION_FAILED" | "SCREENSHOT_FAILED" | "OUTPUT_WRITE_FAILED" | "INTERNAL_ERROR"]
11
+
12
+ DEFAULT_CODE: "INTERNAL_ERROR"
13
+
14
+ def self.for: (untyped exception) -> untyped
15
+
16
+ # Builds the {code:, message:, retryable:, hint:, details:} hash used as Result#error and in
17
+ # recipe action/assertion failures. Works for any StandardError, not only a Bidi2pdf::Error -
18
+ # an unexpected exception still gets INTERNAL_ERROR with retryable/hint defaulted safely.
19
+ def self.describe: (untyped exception, ?details: ::Hash[untyped, untyped]) -> { code: untyped, message: untyped, retryable: untyped, hint: untyped, details: untyped }
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Bidi2pdf
2
+ # Maps an ErrorCodes string to the process exit status the CLI reports. Centralized here so
3
+ # render/diagnose/run share one table instead of each re-deciding what a given failure is worth.
4
+ module ExitCodes
5
+ SUCCESS: 0
6
+
7
+ CLI_ERROR: 2
8
+
9
+ BROWSER_ERROR: 3
10
+
11
+ PAGE_NOT_AS_EXPECTED: 4
12
+
13
+ OUTPUT_ERROR: 6
14
+
15
+ INTERNAL_ERROR: 70
16
+
17
+ CODE_TO_EXIT: { "MISSING_INPUT" => untyped, "MULTIPLE_INPUT_SOURCES" => untyped, "EMPTY_INPUT" => untyped, "INVALID_CONFIG" => untyped, "INVALID_PRINT_OPTION" => untyped, "INVALID_RECIPE" => untyped, "PDF_INSPECTION_UNAVAILABLE" => untyped, "BROWSER_LAUNCH_FAILED" => untyped, "BROWSER_DISCONNECTED" => untyped, "COMMAND_TIMEOUT" => untyped, "NAVIGATION_FAILED" => untyped, "NAVIGATION_TIMEOUT" => untyped, "NAVIGATION_AUTH" => untyped, "NAVIGATION_NOT_FOUND" => untyped, "DNS_ERROR" => untyped, "SELECTOR_NOT_FOUND" => untyped, "PAGE_NOT_AS_EXPECTED" => untyped, "SCRIPT_ERROR" => untyped, "STYLE_ERROR" => untyped, "PDF_GENERATION_FAILED" => untyped, "SCREENSHOT_FAILED" => untyped, "OUTPUT_WRITE_FAILED" => untyped, "INTERNAL_ERROR" => untyped }
18
+
19
+ def self.for: (untyped code) -> untyped
20
+ end
21
+ end
@@ -62,11 +62,20 @@ module Bidi2pdf
62
62
 
63
63
  @chrome_args: untyped
64
64
 
65
+ @diagnose_runner: untyped
66
+
65
67
  # rubocop:disable Metrics/ParameterLists
66
68
  def initialize: (url: untyped, inputfile: untyped, output: untyped, cookies: untyped, headers: untyped, auth: untyped, ?headless: bool, ?port: ::Integer, ?wait_window_loaded: bool, ?wait_network_idle: bool, ?print_options: ::Hash[untyped, untyped], ?remote_browser_url: untyped?, ?network_log_format: ::Symbol, ?chrome_args: untyped) -> void
67
69
 
68
70
  def launch: () -> untyped
69
71
 
72
+ # Like #launch, but navigates only (see SessionRunner#run_diagnose) and leaves the tab open -
73
+ # used by `bidi2pdf diagnose`, which has no PDF to produce. #stop closes it, same as it stops
74
+ # everything else #launch/#diagnose started.
75
+ #
76
+ # @return [Bidi2pdf::Bidi::BrowserTab] the navigated tab.
77
+ def diagnose: () -> untyped
78
+
70
79
  def stop: () -> untyped
71
80
 
72
81
  private
@@ -0,0 +1,38 @@
1
+ module Bidi2pdf
2
+ # Builds the render manifest (--manifest FILE) from a completed Bidi2pdf::Result plus the
3
+ # input/browser/header context around it. Redacts secret-
4
+ # bearing headers and never records cookie values at all.
5
+ class Manifest
6
+ @result: untyped
7
+
8
+ @input: untyped
9
+
10
+ @headers: untyped
11
+
12
+ @navigation_duration_ms: untyped
13
+
14
+ @browser: untyped
15
+
16
+ SCHEMA_VERSION: 1
17
+
18
+ REDACT_HEADER_NAMES: ::Array["authorization" | "proxy-authorization" | "cookie" | "set-cookie" | "x-api-key" | "api-key"]
19
+
20
+ SENSITIVE_SUBSTRINGS: ::Array["token" | "secret" | "password" | "authorization" | "api-key" | "apikey"]
21
+
22
+ def initialize: (result: untyped, input: untyped, ?headers: ::Hash[untyped, untyped], ?navigation_duration_ms: untyped?, ?browser: ::Hash[untyped, untyped]) -> void
23
+
24
+ def self.redact: (untyped headers) -> untyped
25
+
26
+ def self.sensitive?: (untyped key) -> untyped
27
+
28
+ def to_h: () -> untyped
29
+
30
+ def to_json: (*untyped) -> untyped
31
+
32
+ private
33
+
34
+ def output_section: () -> { path: untyped, pages: untyped, bytes: untyped, sha256: untyped }
35
+
36
+ def navigation_section: () -> untyped
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ module Bidi2pdf
2
+ module Notifications
3
+ # Emits one JSON object per line to an IO (stderr for --json-stream, docs/specs/llm-friendly-
4
+ # spec.md section 7) as a render progresses, by subscribing to the same Bidi2pdf::Notifications
5
+ # events Bidi2pdf::ResultCollector and LoggingSubscriber already use - no new instrumentation
6
+ # points needed. #emit_result writes the final "result" event once the caller has a Result.
7
+ class JsonSubscriber
8
+ @io: untyped
9
+
10
+ @start: untyped
11
+
12
+ @registered: untyped
13
+
14
+ SCHEMA_VERSION: 1
15
+
16
+ def initialize: (?io: untyped) -> void
17
+
18
+ def emit_result: (untyped result) -> untyped
19
+
20
+ def unsubscribe: () -> untyped
21
+
22
+ private
23
+
24
+ def now_ms: () -> untyped
25
+
26
+ def elapsed_ms: () -> untyped
27
+
28
+ def subscribe: () -> untyped
29
+
30
+ def on_navigate: (untyped event) -> untyped
31
+
32
+ def on_page_loaded: (untyped _event) -> untyped
33
+
34
+ def on_network_idle: (untyped _event) -> untyped
35
+
36
+ def on_console: (untyped event) -> untyped
37
+
38
+ def on_print: (untyped _event) -> untyped
39
+
40
+ def on_screenshot: (untyped _event) -> untyped
41
+
42
+ def write: (untyped fields) -> untyped
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ module Bidi2pdf
2
+ # Lazy, optional wrapper around the pdf-reader gem. pdf-reader is deliberately NOT a bidi2pdf
3
+ # runtime dependency - render works fine without it,
4
+ # just with `pages` left nil and a warning, and a recipe assertion that needs it fails
5
+ # INVALID_RECIPE at validation time rather than crashing mid-render. The official Docker images
6
+ # (docker/Dockerfile, docker/Dockerfile.slim) install it, so a render inside one of them - the
7
+ # environment an agent or CI job actually uses - always has page counts and PDF assertions
8
+ # available without anyone opting in by hand.
9
+ module PdfInspection
10
+ self.@available: untyped
11
+
12
+ def self.available?: () -> untyped
13
+
14
+ # @param bytes [String, nil] raw PDF bytes.
15
+ # @return [Integer, nil] page count, or nil when pdf-reader is unavailable, bytes is nil,
16
+ # or the PDF is malformed.
17
+ def self.page_count: (untyped bytes) -> untyped
18
+
19
+ # @param bytes [String, nil] raw PDF bytes.
20
+ # @return [String, nil] extracted text across all pages, joined with newlines.
21
+ def self.text: (untyped bytes) -> untyped
22
+
23
+ # Clears the memoized availability check - test-only, so a spec can exercise both branches
24
+ # of #available? regardless of load order elsewhere in the process.
25
+ def self.reset_for_testing!: () -> (untyped | nil)
26
+
27
+ private
28
+
29
+ def self.load_pdf_reader: () -> untyped
30
+
31
+ def self.reader_for: (untyped bytes) -> untyped
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ module Bidi2pdf
2
+ class Recipe
3
+ # Reads a recipe file (YAML, or JSON when the extension is .json) into a plain Hash. Never
4
+ # launches a browser - a malformed file fails right here.
5
+ #
6
+ # Security: YAML is parsed with Psych.safe_load and no extra options. Its defaults already
7
+ # refuse both custom type tags (no arbitrary Ruby object instantiation) and aliases - the
8
+ # latter matters concretely: a small file using nested YAML anchors/aliases ("billion laughs")
9
+ # can expand to millions of elements in memory; confirmed live, a 273-byte such file produced
10
+ # 10 million array elements in under 2ms. Neither aliases nor a Date/Time value are used by any
11
+ # field a recipe defines, so neither is turned back on "just in case" - only what the format
12
+ # actually needs is permitted. MAX_BYTES bounds the file itself before it is even parsed.
13
+ module Loader
14
+ MAX_BYTES: 1048576
15
+
16
+ # rubocop:disable-next Metrics/AbcSize
17
+ def self.load: (untyped path) -> untyped
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,92 @@
1
+ module Bidi2pdf
2
+ class Recipe
3
+ # Runs a validated recipe's actions and assertions against a navigated tab. A thin dispatch
4
+ # over BrowserTab's own methods, not a parallel action/assertion class hierarchy - a step
5
+ # that needs something BrowserTab cannot do belongs there, not here.
6
+ class Runner
7
+ @recipe: untyped
8
+
9
+ @tab: untyped
10
+
11
+ @collector: untyped
12
+
13
+ @state: untyped
14
+
15
+ @pdf_bytes: untyped
16
+
17
+ # Raised on the first failing action/assertion; #entries carries every step run so far
18
+ # (successes and the failure itself), for the caller to report in the recipe result.
19
+ class StepFailure < StandardError
20
+ @entries: untyped
21
+
22
+ @cause: untyped
23
+
24
+ attr_reader entries: untyped
25
+
26
+ attr_reader cause: untyped
27
+
28
+ def initialize: (untyped entries, untyped cause) -> void
29
+ end
30
+
31
+ attr_reader state: untyped
32
+
33
+ attr_accessor pdf_bytes: untyped
34
+
35
+ # @param recipe [Bidi2pdf::Recipe]
36
+ # @param tab [Bidi2pdf::Bidi::BrowserTab] a navigated tab (e.g. from Launcher#diagnose)
37
+ # @param collector [Bidi2pdf::ResultCollector] source of #console/#network_failures -
38
+ # exposed live, so assertions can read them mid-run (see ResultCollector's own docs)
39
+ def initialize: (recipe: untyped, tab: untyped, collector: untyped) -> void
40
+
41
+ # @return [Array<Hash>] one entry per action actually run
42
+ # @raise [StepFailure] on the first failing action
43
+ def run_actions: () -> untyped
44
+
45
+ # @return [Array<Hash>] one entry per assertion actually run
46
+ # @raise [StepFailure] on the first failing assertion
47
+ def run_assertions: () -> untyped
48
+
49
+ private
50
+
51
+ # rubocop:disable-next Metrics/AbcSize
52
+ def run_steps: (untyped steps) { (untyped, untyped) -> untyped } -> untyped
53
+
54
+ def now_ms: () -> untyped
55
+
56
+ def elapsed_ms: (untyped start) -> untyped
57
+
58
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
59
+ def dispatch_action: (untyped name, untyped opts) -> untyped
60
+
61
+ def action_wait_for: (untyped opts) -> (nil | untyped)
62
+
63
+ def wait_for_condition: (untyped opts) -> ("document.querySelector('.pagedjs_page')" | ::String | untyped)
64
+
65
+ def action_click: (untyped opts) -> (nil | untyped)
66
+
67
+ def action_evaluate: (untyped opts) -> untyped
68
+
69
+ # rubocop:disable-next Metrics/CyclomaticComplexity
70
+ def dispatch_assertion: (untyped name, untyped step) -> untyped
71
+
72
+ def assertion_selector_exists: (untyped opts) -> ::Array[untyped | { selector: untyped }]
73
+
74
+ def assertion_text_present: (untyped opts) -> ::Array[untyped | { text: untyped }]
75
+
76
+ def assertion_no_console_errors: () -> ::Array[untyped]
77
+
78
+ # Filtering by resource type (a `types:` option) is not implemented: Bidi2pdf::Bidi::
79
+ # NetworkEvent carries an HTTP method, not a resource type, so there is nothing to filter
80
+ # by yet. Every captured failure counts.
81
+ def assertion_no_network_failures: () -> ::Array[untyped]
82
+
83
+ def assertion_fonts_loaded: () -> ::Array[untyped | nil]
84
+
85
+ def assertion_page_count: (untyped expected) -> ::Array[untyped | { expected: untyped, actual: untyped }]
86
+
87
+ def assertion_pdf_text_present: (untyped opts) -> ::Array[untyped | { text: untyped }]
88
+
89
+ def assertion_pdf_not_blank: () -> ::Array[untyped | { page_count: untyped, text_present: untyped }]
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,85 @@
1
+ module Bidi2pdf
2
+ class Recipe
3
+ # Structural validation against Schema::RECIPE itself, not a hand-duplicated parallel list of
4
+ # rules. Validator's other #check_* methods each enforce one *semantic* rule (an action name
5
+ # is known, a source key is truthy, a presence assertion is `true`) with a friendly, specific
6
+ # message - none of them enforce the schema's own `additionalProperties: false` at every
7
+ # level, so an extra, unrecognized key sitting alongside an otherwise-valid one used to pass
8
+ # silently even though `bidi2pdf schema recipe` would reject it (`{url: "...", stdin: false}`,
9
+ # `{wait_for: {...}, extra: 1}` - two action names in one step, `wait_for: {selector: "#x",
10
+ # bogus: "y"}`). #check_shape (see Validator) closes that whole class of gap generically by
11
+ # walking the schema the CLI already publishes, instead of re-describing "what's allowed" a
12
+ # second time in Ruby - the schema is the one place that description can't drift from itself.
13
+ #
14
+ # Deliberately not a general JSON Schema engine and not a gem dependency: only the keywords
15
+ # Schema::RECIPE actually uses (type, const, enum, required, additionalProperties, properties,
16
+ # oneOf, anyOf, items, minimum, maximum). Extend the keyword list only if a future schema
17
+ # branch genuinely needs one this doesn't cover yet.
18
+ module SchemaShape
19
+ TYPE_CHECKS: { "object" => untyped, "array" => untyped, "string" => untyped, "integer" => untyped, "number" => untyped, "boolean" => untyped, "null" => untyped }
20
+
21
+ CHECKS: ::Array[:const_ok? | :enum_ok? | :type_ok? | :one_of_ok? | :any_of_ok? | :required_ok? | :additional_properties_ok? | :properties_ok? | :items_ok? | :bounds_ok?]
22
+
23
+ # @return [Boolean] whether value satisfies schema
24
+ def self?.matches?: (untyped schema, untyped value) -> untyped
25
+
26
+ VIOLATION_CHECKS: ::Array[:oneof_violation | :anyof_violation | :required_violation | :additional_properties_violation | :const_violation | :enum_violation | :type_violation | :bounds_violation | :nested_violation]
27
+
28
+ # @return [Hash, nil] {path:, reason:} for the first violation a depth-first walk finds, or
29
+ # nil if value already satisfies schema. Not necessarily *the* most relevant violation
30
+ # when several exist at once - good enough to point an agent at the right neighborhood,
31
+ # not a substitute for reading `bidi2pdf schema recipe` for the exact shape.
32
+ def self?.first_violation: (untyped schema, untyped value, ?untyped path) -> (nil | untyped | { path: untyped, reason: "does not match the schema" })
33
+
34
+ def self?.const_ok?: (untyped schema, untyped value) -> untyped
35
+
36
+ def self?.enum_ok?: (untyped schema, untyped value) -> untyped
37
+
38
+ def self?.type_ok?: (untyped schema, untyped value) -> (true | untyped)
39
+
40
+ def self?.one_of_ok?: (untyped schema, untyped value) -> (true | untyped)
41
+
42
+ def self?.any_of_ok?: (untyped schema, untyped value) -> (true | untyped)
43
+
44
+ def self?.required_ok?: (untyped schema, untyped value) -> (true | untyped)
45
+
46
+ def self?.additional_properties_ok?: (untyped schema, untyped value) -> (true | untyped)
47
+
48
+ def self?.properties_ok?: (untyped schema, untyped value) -> (true | untyped)
49
+
50
+ def self?.items_ok?: (untyped schema, untyped value) -> (true | untyped)
51
+
52
+ def self?.bounds_ok?: (untyped schema, untyped value) -> (true | untyped)
53
+
54
+ def self?.display_path: (untyped path) -> untyped
55
+
56
+ # When zero branches match but exactly one is "plausible" (its own required key(s) are
57
+ # present, ignoring additionalProperties), recurse into that one branch for a specific
58
+ # reason/path instead of a bare "matched 0 of N" - this is what turns "actions[0] doesn't
59
+ # match any known action" into "actions[0].wait_for: unknown key(s): bogus".
60
+ def self?.oneof_violation: (untyped schema, untyped value, untyped path) -> (nil | untyped)
61
+
62
+ def self?.single_plausible_violation: (untyped schema, untyped value, untyped path) -> (nil | untyped)
63
+
64
+ def self?.anyof_violation: (untyped schema, untyped value, untyped path) -> (nil | { path: untyped, reason: ::String })
65
+
66
+ def self?.required_violation: (untyped schema, untyped value, untyped path) -> (nil | { path: untyped, reason: ::String })
67
+
68
+ def self?.additional_properties_violation: (untyped schema, untyped value, untyped path) -> (nil | { path: untyped, reason: ::String })
69
+
70
+ def self?.const_violation: (untyped schema, untyped value, untyped path) -> (nil | { path: untyped, reason: ::String })
71
+
72
+ def self?.enum_violation: (untyped schema, untyped value, untyped path) -> (nil | { path: untyped, reason: ::String })
73
+
74
+ def self?.type_violation: (untyped schema, untyped value, untyped path) -> (nil | { path: untyped, reason: ::String })
75
+
76
+ def self?.bounds_violation: (untyped schema, untyped value, untyped path) -> (nil | { path: untyped, reason: ::String })
77
+
78
+ def self?.nested_violation: (untyped schema, untyped value, untyped path) -> untyped
79
+
80
+ def self?.properties_violation: (untyped schema, untyped value, untyped path) -> (nil | untyped)
81
+
82
+ def self?.items_violation: (untyped schema, untyped value, untyped path) -> (nil | untyped)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,54 @@
1
+ module Bidi2pdf
2
+ class Recipe
3
+ # Validates a loaded recipe against the schema `bidi2pdf schema recipe` publishes, before any
4
+ # browser is launched - `bidi2pdf run --validate` runs exactly this and nothing else. Every
5
+ # failure is Bidi2pdf::InvalidRecipeError (or Bidi2pdf::PdfInspectionUnavailableError for a
6
+ # PDF assertion without pdf-reader installed), details carrying a `path` an agent can act on
7
+ # directly.
8
+ class Validator
9
+ @recipe: untyped
10
+
11
+ def initialize: (untyped recipe) -> void
12
+
13
+ def validate!: () -> untyped
14
+
15
+ private
16
+
17
+ def check_version: () -> (nil | untyped)
18
+
19
+ def check_top_level_keys: () -> (nil | untyped)
20
+
21
+ def check_source: () -> untyped
22
+
23
+ def check_steps: (untyped section, untyped steps, untyped known) -> untyped
24
+
25
+ # Mirrors Schema::RECIPE_ACTIONS' own wait_for oneOf: exactly one of selector/paged_js/script.
26
+ # --validate runs this check itself rather than relying on Runner#wait_for_condition, whose
27
+ # own "needs one of" error only fires mid-run (after a browser is already launched) and does
28
+ # not catch "more than one given" at all - it just silently prefers paged_js, then selector.
29
+ def check_wait_for_conditions: () -> untyped
30
+
31
+ # Mirrors Schema::RECIPE_ASSERT's own const: true for the 4 presence-only assertions -
32
+ # Runner never reads the value beside them, so anything but `true` is misleading rather than
33
+ # merely unusual (see Recipe::PRESENCE_ONLY_ASSERTIONS' own comment).
34
+ def check_presence_assertions: () -> untyped
35
+
36
+ def check_pdf_assertions_need_pdf_inspection: () -> (nil | untyped)
37
+
38
+ def check_output: () -> (nil | untyped)
39
+
40
+ def check_print_options: () -> untyped
41
+
42
+ def symbolize: (untyped hash) -> untyped
43
+
44
+ # Runs last, deliberately: everything above gives a friendlier, more specific message for
45
+ # the case it already knows about (an unknown action name, a non-true presence assertion, a
46
+ # real PrintParametersValidator error, ...), so this only ever fires for a shape violation
47
+ # none of them catches - an extra/unrecognized key sitting alongside an otherwise-valid one
48
+ # (SchemaShape's own doc comment has the concrete examples this closes).
49
+ def check_shape: () -> (nil | untyped)
50
+
51
+ def fail!: (untyped reason, path: untyped) -> untyped
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,72 @@
1
+ module Bidi2pdf
2
+ # A recipe is a rendering contract: the waits and preparation a page needs before it is printed,
3
+ # and the properties the resulting PDF must have - not a general browser-automation script.
4
+ # `bidi2pdf run recipe.yml` is Loader -> (an invalid recipe stops here, no browser ever
5
+ # launches) -> Validator -> Runner.
6
+ class Recipe
7
+ @data: untyped
8
+
9
+ @path: untyped
10
+
11
+ KNOWN_TOP_LEVEL_KEYS: ::Array["version" | "source" | "browser" | "headers" | "cookies" | "auth" | "actions" | "assert" | "print" | "output"]
12
+
13
+ KNOWN_ACTIONS: ::Array["wait_for" | "click" | "evaluate" | "inject_script" | "inject_style" | "set_viewport" | "wait_network_idle"]
14
+
15
+ PAGE_ASSERTIONS: ::Array["selector_exists" | "text_present" | "no_console_errors" | "no_network_failures" | "fonts_loaded"]
16
+
17
+ PDF_ASSERTIONS: ::Array["page_count" | "pdf_text_present" | "pdf_not_blank"]
18
+
19
+ KNOWN_ASSERTIONS: untyped
20
+
21
+ KNOWN_OUTPUTS: ::Array["pdf" | "manifest" | "screenshot"]
22
+
23
+ # These four assertions are pure presence checks - Runner never reads the value beside them
24
+ # (see #assertion_no_console_errors and friends), so a bare key is really what's being
25
+ # asserted. `false`/anything but `true` is therefore misleading, not merely unusual: schema
26
+ # recipe encodes this as `const: true` and Validator#check_presence_assertions enforces it too.
27
+ PRESENCE_ONLY_ASSERTIONS: ::Array["no_console_errors" | "no_network_failures" | "fonts_loaded" | "pdf_not_blank"]
28
+
29
+ attr_reader data: untyped
30
+
31
+ attr_reader path: untyped
32
+
33
+ def self.load: (untyped path) -> untyped
34
+
35
+ def initialize: (untyped data, ?path: untyped?) -> void
36
+
37
+ def validate!: () -> untyped
38
+
39
+ def actions: () -> untyped
40
+
41
+ def assertions: () -> untyped
42
+
43
+ def source: () -> untyped
44
+
45
+ def browser_options: () -> untyped
46
+
47
+ def headers: () -> untyped
48
+
49
+ def cookies: () -> untyped
50
+
51
+ def auth: () -> untyped
52
+
53
+ def print_options: () -> untyped
54
+
55
+ def output: () -> untyped
56
+
57
+ def needs_pdf?: () -> untyped
58
+
59
+ def self.step_name: (untyped step) -> untyped
60
+
61
+ def step_name: (untyped step) -> untyped
62
+
63
+ # The value beside a step's name - a Hash of options (`wait_for: {selector: ..., timeout:
64
+ # ...}`), a bare scalar (`page_count: 2`, `no_console_errors: true`), or nil.
65
+ def step_value: (untyped step) -> (untyped | nil)
66
+
67
+ # step_value coerced to a Hash of options - most actions/assertions take this shape; a bare
68
+ # scalar step (page_count: 2, no_console_errors: true) has no options of its own to offer here,
69
+ # so callers that need the scalar itself use #step_value directly.
70
+ def step_options: (untyped step) -> untyped
71
+ end
72
+ end
@@ -0,0 +1,71 @@
1
+ module Bidi2pdf
2
+ # Structured result of a CLI command (render/diagnose/run) - the shape behind --json output, a
3
+ # render manifest, and the final "result" event of --json-stream. Construct via .success/
4
+ # .failure rather than .new directly so every Result is unambiguously one or the other.
5
+ class Result
6
+ @command: untyped
7
+
8
+ @ok: untyped
9
+
10
+ @output: untyped
11
+
12
+ @bytes: untyped
13
+
14
+ @sha256: untyped
15
+
16
+ @pages: untyped
17
+
18
+ @duration_ms: untyped
19
+
20
+ @navigation: untyped
21
+
22
+ @console: untyped
23
+
24
+ @network_failures: untyped
25
+
26
+ @warnings: untyped
27
+
28
+ @error: untyped
29
+
30
+ @metadata: untyped
31
+
32
+ SCHEMA_VERSION: 1
33
+
34
+ attr_reader command: untyped
35
+
36
+ attr_reader output: untyped
37
+
38
+ attr_reader bytes: untyped
39
+
40
+ attr_reader sha256: untyped
41
+
42
+ attr_reader pages: untyped
43
+
44
+ attr_reader duration_ms: untyped
45
+
46
+ attr_reader navigation: untyped
47
+
48
+ attr_reader console: untyped
49
+
50
+ attr_reader network_failures: untyped
51
+
52
+ attr_reader warnings: untyped
53
+
54
+ attr_reader error: untyped
55
+
56
+ attr_reader metadata: untyped
57
+
58
+ # rubocop:disable-next Metrics/ParameterLists, Naming/MethodParameterName
59
+ def initialize: (command: untyped, ok: untyped, ?output: untyped?, ?bytes: untyped?, ?sha256: untyped?, ?pages: untyped?, ?duration_ms: untyped?, ?navigation: untyped?, ?console: untyped, ?network_failures: untyped, ?warnings: untyped, ?error: untyped?, ?metadata: ::Hash[untyped, untyped]) -> void
60
+
61
+ def self.success: (**untyped) -> untyped
62
+
63
+ def self.failure: (error: untyped, **untyped) -> untyped
64
+
65
+ def ok?: () -> untyped
66
+
67
+ def to_h: () -> untyped
68
+
69
+ def to_json: (*untyped) -> untyped
70
+ end
71
+ end