ghostcrawl 2.2.2 → 2.3.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: ff09081da25b9c78b261437d5bc26c2a4b25f3e727a53e8393f1adb00aa6536c
4
- data.tar.gz: ad0869fe147e1c33a49fadd364bc93ce5857fd73eb03b584d4c1ce618e1092a9
3
+ metadata.gz: 482db5d9b31ab1b7293fc19b6834be23062a37d6ef8632fae462e369e5dfc244
4
+ data.tar.gz: 01eababfa0e5cd0ecc5ca39386c4b7a8cdbe2d8f0ff7f079382c89fed5859c9c
5
5
  SHA512:
6
- metadata.gz: '085820b153fb6b70ea26f6e866787388835cfec7e4cce86498f83551abe6b4c8800b5eb14f86203c59ca77b7e70cffbd459d0d84f7a75fc69f5337914bcf60e6'
7
- data.tar.gz: 11a00207fc7469d02bec976018c8e627ea11c0da0ce78d4dc5a6d5b76fced17a5f3a37053159e0bde94dd0c44e4d3e273e7cf57cdac1d652a410bcba4da814ff
6
+ metadata.gz: 866336d0c4c835f6ccaafd9a444c03eb4931b97cbf4b65ca003b01ff5d374a68b06500542805951ee25c3276adf64b0d16c433a956f0dbbe8bebd91ef10e0211
7
+ data.tar.gz: 250b128f21670a470961468a6da62c1647495d7b4065a6d1fbe4f13328ac680d7b40ebc6acfd68338e88fa9f8bbfb99ec88ea0d5f6f6ebb56cff56261800ea41
data/LICENSE CHANGED
@@ -1,46 +1,21 @@
1
- GhostCrawl Software License
2
-
3
- Copyright (c) 2026 GhostCrawl, LLC. All rights reserved.
4
-
5
- This software including the GhostCrawl SDKs, command-line interface, example
6
- code, documentation, and the GhostCrawl self-host image (collectively, the
7
- "Software") is the proprietary property of GhostCrawl, LLC ("GhostCrawl") and
8
- its licensors. It is licensed, not sold.
9
-
10
- 1. LICENSE GRANT. Subject to your continuous compliance with this License and the
11
- GhostCrawl Terms of Service (https://docs.ghostcrawl.io/legal/terms),
12
- GhostCrawl grants you a limited, revocable, non-exclusive, non-transferable,
13
- and non-sublicensable license to install and use the Software, including
14
- within your own applications, solely to access and use the GhostCrawl service
15
- under a valid GhostCrawl account.
16
-
17
- 2. RESTRICTIONS. Except as expressly permitted in Section 1, you may not, in
18
- whole or in part: (a) copy the Software except as strictly necessary to use it
19
- as permitted; (b) modify, adapt, translate, or create derivative works of the
20
- Software; (c) distribute, publish, sublicense, sell, resell, rent, lease,
21
- lend, or otherwise transfer the Software, or make it available to any third
22
- party, including as a hosted or managed service; (d) reverse engineer,
23
- decompile, disassemble, or otherwise attempt to derive the source code,
24
- structure, or internal design of any compiled component of the Software;
25
- (e) circumvent, disable, or tamper with any license, authentication,
26
- usage-metering, watermarking, or security mechanism; or (f) remove, alter, or
27
- obscure any proprietary, copyright, or attribution notice.
28
-
29
- 3. OWNERSHIP. GhostCrawl and its licensors retain all right, title, and interest
30
- in and to the Software and all intellectual property rights therein. No rights
31
- are granted to you except as expressly set out in this License. All rights
32
- reserved.
33
-
34
- 4. TERMINATION. This License terminates automatically, without notice, if you
35
- breach any of its terms or cease to maintain a valid GhostCrawl account where
36
- one is required. Upon termination you must cease all use of the Software and
37
- destroy all copies in your possession or control.
38
-
39
- 5. NO WARRANTY; LIMITATION OF LIABILITY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT
40
- WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. TO THE MAXIMUM EXTENT PERMITTED BY
41
- LAW, GHOSTCRAWL AND ITS LICENSORS WILL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR
42
- OTHER LIABILITY ARISING FROM OR IN CONNECTION WITH THE SOFTWARE OR ITS USE. The
43
- GhostCrawl Terms of Service and Acceptable Use Policy
44
- (https://docs.ghostcrawl.io/legal/terms) are incorporated by reference.
45
-
46
- For licensing inquiries, contact legal@ghostcrawl.io.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GhostCrawl, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -29,9 +29,11 @@ client = Ghostcrawl::Client.new(token: "gck_live_YOUR_KEY")
29
29
  result = client.scrape(url: "https://example.com", format: "markdown")
30
30
  puts result["content"]
31
31
 
32
- # Start a crawl run
33
- run = client.crawl_runs.start(url: "https://example.com", max_depth: 2, max_pages: 50)
34
- puts "run_id: #{run["run_id"]}"
32
+ # Start a crawl run and wait for it to finish — the server blocks until the run
33
+ # is terminal (no client-side poll-with-sleep loop).
34
+ run = client.crawl_runs.start(url: "https://example.com", max_depth: 2, max_pages: 50,
35
+ wait: true, timeout: 300)
36
+ puts "#{run["run_id"]} → #{run["status"]}"
35
37
 
36
38
  # Web search
37
39
  results = client.search(query: "latest AI research", engine: "google", limit: 10)
@@ -72,6 +74,40 @@ data = client.extract(
72
74
  puts "#{data["name"]} — $#{data["price"]}"
73
75
  ```
74
76
 
77
+ ## Crawl runs — wait for completion
78
+
79
+ Crawl runs are asynchronous. Instead of hand-writing a poll-with-sleep loop, use
80
+ the event-driven wait API: the **server** blocks until the run reaches a terminal
81
+ state (`completed` / `failed` / `cancelled`) and returns the finished run.
82
+
83
+ ```ruby
84
+ # Start and wait in one call — sends wait_until: "completed"
85
+ run = client.crawl_runs.start(
86
+ url: "https://example.com",
87
+ max_depth: 2,
88
+ max_pages: 50,
89
+ wait: true, # block until terminal (no client sleep loop)
90
+ timeout: 300 # give up after 300s, returning the still-running run
91
+ )
92
+ puts run["status"] # "completed" | "failed" | "cancelled"
93
+
94
+ # Or wait on a run you already started (e.g. from a webhook or another process):
95
+ run = client.crawl_runs.wait_for_completion(run_id, timeout: 300)
96
+
97
+ # Deep crawl supports the same wait: keyword:
98
+ run = client.crawl(url: "https://example.com", wait: true, timeout: 300)
99
+ ```
100
+
101
+ Under the hood `wait_for_completion` chains server-blocking
102
+ `GET /v1/crawl-runs/{run_id}?wait=true&timeout_s=N` requests until the run is
103
+ terminal or your `timeout` deadline — there is no `sleep` between checks. On
104
+ timeout the current non-terminal run is returned, so you can simply call
105
+ `wait_for_completion` again to keep waiting.
106
+
107
+ **Prefer webhooks for fire-and-forget crawls.** Register an endpoint with
108
+ `client.webhooks.create(url:, event_types: ["crawl.completed"])` and let GhostCrawl
109
+ POST the result to you instead of holding a connection open.
110
+
75
111
  ## Error handling
76
112
 
77
113
  ```ruby
@@ -113,7 +149,7 @@ client = Ghostcrawl::Client.new(
113
149
  | Deep crawl | `client.crawl(url:)` | Crawl a site depth-first |
114
150
  | URL map | `client.map(url:)` | Discover all reachable URLs |
115
151
  | Account | `client.me` | Get account info and usage |
116
- | Crawl runs | `client.crawl_runs` | start, list, get\_run, cancel |
152
+ | Crawl runs | `client.crawl_runs` | start (with `wait:`), wait\_for\_completion, list, get, cancel |
117
153
  | Sessions | `client.sessions` | create, list, extend, release |
118
154
  | Profiles | `client.profiles` | list, get\_profile, create, update, delete |
119
155
  | Webhooks | `client.webhooks` | list, get\_webhook, create, delete, rotate\_secret |
@@ -0,0 +1,9 @@
1
+ module Ghostcrawl
2
+ module V1
3
+ ##
4
+ # Namespace retained as the raw-response sentinel used by binary-returning
5
+ # operations (datasets/page/har/discovery) and the long-poll wait factory.
6
+ module Binary
7
+ end
8
+ end
9
+ end
@@ -2,7 +2,7 @@ require 'microsoft_kiota_abstractions'
2
2
  require_relative '../ghostcrawl'
3
3
  require_relative './audit/audit_request_builder'
4
4
  require_relative './billing/billing_request_builder'
5
- require_relative './binary/binary_request_builder'
5
+ require_relative './binary/binary'
6
6
  require_relative './budgets/budgets_request_builder'
7
7
  require_relative './cdp/cdp_request_builder'
8
8
  require_relative './contact/contact_request_builder'
@@ -50,11 +50,6 @@ module Ghostcrawl
50
50
  return Ghostcrawl::V1::Billing::BillingRequestBuilder.new(@path_parameters, @request_adapter)
51
51
  end
52
52
  ##
53
- # The binary property
54
- def binary()
55
- return Ghostcrawl::V1::Binary::BinaryRequestBuilder.new(@path_parameters, @request_adapter)
56
- end
57
- ##
58
53
  # The budgets property
59
54
  def budgets()
60
55
  return Ghostcrawl::V1::Budgets::BudgetsRequestBuilder.new(@path_parameters, @request_adapter)
@@ -16,6 +16,7 @@
16
16
  # client = Ghostcrawl::Client.new(token: "gck_live_YOUR_KEY")
17
17
  # result = client.scrape(url: "https://example.com")
18
18
 
19
+ require "json"
19
20
  require "microsoft_kiota_abstractions"
20
21
  require "microsoft_kiota_faraday"
21
22
  require "microsoft_kiota_serialization_json"
@@ -57,6 +58,72 @@ module Ghostcrawl
57
58
  super
58
59
  end
59
60
  end
61
+
62
+ # Fix 1b: the pinned Kiota writer's write_collection_of_object_values calls
63
+ # `self.write_object_value(nil, v).writer` on each element — it relies on
64
+ # write_object_value(nil, ...) returning a *writer object* that responds to
65
+ # `.writer`. Our write_object_value override above (Fix 1) changes that
66
+ # nil-key contract to serialize-into-self and return the serialize result
67
+ # (a Hash / nil), so the upstream `.writer` call raises
68
+ # `NoMethodError: undefined method 'writer' for {}:Hash`. That aborts
69
+ # serialize for ANY response model carrying a collection of typed objects
70
+ # (WebhookListResponse#items, etc.), which ResponseHelper.serialize_parsable
71
+ # then swallows -> the facade returns {} and silently drops the whole list.
72
+ #
73
+ # Fix: serialize each element into its OWN fresh writer and collect the raw
74
+ # per-element hashes directly, never depending on the write_object_value
75
+ # return contract. Mirrors the non-nil upstream branch (line 154) without the
76
+ # broken `.writer` deref.
77
+ def write_collection_of_object_values(key, values)
78
+ return unless values
79
+ hashes = values.map do |v|
80
+ temp = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new
81
+ v.serialize(temp)
82
+ temp.writer
83
+ end
84
+ if key.nil?
85
+ hashes
86
+ else
87
+ writer[key] = hashes
88
+ end
89
+ end
90
+
91
+ # Fix 1c: the pinned Kiota writer's write_any_value (used by
92
+ # write_additional_data, i.e. EVERY field AdditionalDataBody sends) has no
93
+ # case for a plain Hash and mis-handles an Array of Hashes:
94
+ # * a Hash hits the `value.is_a? Object` branch -> `return value.to_s`,
95
+ # which returns a string but NEVER assigns @writer[key] -> the whole
96
+ # nested object is SILENTLY DROPPED from the request body.
97
+ # * an Array is sent to write_collection_of_primitive_values, which mangles
98
+ # any non-primitive (Hash) element.
99
+ # This drops/mangles every nested field the facade sends: scrape(extract_schema),
100
+ # extract(schema), crawl/crawl_runs opts, datasets.append(rows: [ {..} ]),
101
+ # schedules.create(job_params: {..}) — the last of which the API REQUIRES,
102
+ # so the request 422s ("job_params required") even though the caller supplied it.
103
+ #
104
+ # Fix: intercept Hash and Array here and write a fully-recursed, JSON-native
105
+ # structure straight into @writer[key]. The writer emits @writer.to_json at the
106
+ # end, so plain nested Hash/Array values serialize correctly. Everything else
107
+ # falls through to the upstream primitive handling.
108
+ def write_any_value(key, value)
109
+ if value.is_a?(Hash) || value.is_a?(Array)
110
+ return super unless key # nil-key arrays keep upstream behavior
111
+ writer[key] = KiotaWriterFix.jsonable(value)
112
+ return
113
+ end
114
+ super
115
+ end
116
+
117
+ # Recursively converts a value into a JSON-native structure (Hash/Array of
118
+ # plain scalars), so it round-trips through @writer.to_json intact.
119
+ # @api private
120
+ def self.jsonable(value)
121
+ case value
122
+ when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = jsonable(v) }
123
+ when Array then value.map { |v| jsonable(v) }
124
+ else value
125
+ end
126
+ end
60
127
  end
61
128
 
62
129
  # Patch the writer class once, idempotently
@@ -190,17 +257,130 @@ module Ghostcrawl
190
257
  when NilClass
191
258
  {}
192
259
  else
193
- # Typed Parsable: try serializing to JSON then parsing back
194
- if value.respond_to?(:additional_data) && value.additional_data
195
- value.additional_data.transform_values { |v| to_hash(v) }
196
- elsif value.respond_to?(:to_h)
197
- value.to_h.transform_values { |v| to_hash(v) }
198
- else
199
- value
200
- end
260
+ # Typed Parsable (Kiota-generated model). Kiota models keep their spec
261
+ # fields in typed instance variables, NOT in +additional_data+ (which is
262
+ # only the overflow bucket for unmapped keys). Reading +additional_data+
263
+ # alone therefore returns +{}+ for a fully-typed response (e.g.
264
+ # MapResponse{@links,@success}, WebhookListResponse{@items,@total}),
265
+ # silently dropping the real payload.
266
+ #
267
+ # Recover the typed fields by round-tripping the model through its own
268
+ # +serialize+ (the Parsable contract) into a JSON writer and parsing the
269
+ # result back to a plain Hash. Then overlay any +additional_data+ (unmapped
270
+ # keys the server sent that the model didn't declare). Degrade gracefully:
271
+ # a model whose +serialize+ raises (e.g. an unresolved composed-type member)
272
+ # falls back to the prior additional_data/to_h behavior so nothing regresses.
273
+ parsable_to_hash(value)
274
+ end
275
+ end
276
+
277
+ # Converts a typed Kiota Parsable into a plain Hash by serializing it via its
278
+ # own +serialize+ and parsing the emitted JSON, then merging +additional_data+.
279
+ # Falls back to +additional_data+ / +to_h+ / the value itself on any failure.
280
+ # @api private
281
+ def self.parsable_to_hash(value)
282
+ serialized = serialize_parsable(value)
283
+ if serialized.is_a?(Hash)
284
+ extra = (value.additional_data if value.respond_to?(:additional_data)) || {}
285
+ merged = serialized.merge(extra.transform_keys(&:to_s))
286
+ return merged.transform_values { |v| to_hash(v) }
287
+ end
288
+
289
+ if value.respond_to?(:additional_data) && value.additional_data && !value.additional_data.empty?
290
+ value.additional_data.transform_values { |v| to_hash(v) }
291
+ elsif value.respond_to?(:to_h)
292
+ value.to_h.transform_values { |v| to_hash(v) }
293
+ elsif value.respond_to?(:additional_data) && value.additional_data
294
+ value.additional_data.transform_values { |v| to_hash(v) }
295
+ else
296
+ value
201
297
  end
202
298
  end
203
299
 
300
+ # Serializes a Parsable to a Ruby Hash using the pinned Kiota JSON writer.
301
+ # Returns the parsed Hash, or +nil+ when the model isn't a serializable
302
+ # Parsable or serialization/parse fails (caller then falls back).
303
+ # @api private
304
+ def self.serialize_parsable(value)
305
+ return nil unless value.respond_to?(:serialize)
306
+
307
+ writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new
308
+ value.serialize(writer)
309
+ content = writer.get_serialized_content
310
+ json = content.is_a?(String) ? content : content.read
311
+ return nil if json.nil? || json.empty?
312
+
313
+ parsed = JSON.parse(json)
314
+ parsed.is_a?(Hash) ? parsed : nil
315
+ rescue StandardError
316
+ nil
317
+ end
318
+
319
+ # Executes a request that returns NO response body (an HTTP 204, or any DELETE
320
+ # whose spec maps the success response to +void+) and returns a plain Hash.
321
+ #
322
+ # Why this exists — two upstream defects in the pinned Kiota Ruby runtime that
323
+ # make the generated void-DELETE builders unusable:
324
+ #
325
+ # 1. The generated +delete+ for a void response calls
326
+ # +send_async(request_info, nil, ...)+, and the Faraday adapter hard-raises
327
+ # +"factory cannot be null"+ BEFORE the request is ever sent — so the DELETE
328
+ # never reaches the server and the caller gets a bare +StandardError+
329
+ # (not a {GhostcrawlError}).
330
+ # 2. For void DELETEs the adapter DOES send maps to +Binary+, the response is an
331
+ # empty 204 body, and +get_root_parse_node+ then feeds +""+ to the JSON parser,
332
+ # raising +JSON::ParserError+ on a perfectly successful delete.
333
+ #
334
+ # This helper reuses the adapter's OWN public request pipeline
335
+ # (+convert_to_native_request_async+ applies base-url + auth, +run_request+
336
+ # sends it) but skips body deserialization entirely: a 2xx returns +{}+ (or the
337
+ # decoded JSON when the server did send a body, e.g. +{"deleted":true}+), and a
338
+ # >=400 is translated into the documented typed {GhostcrawlError} hierarchy.
339
+ #
340
+ # @param adapter [MicrosoftKiotaFaraday::FaradayRequestAdapter]
341
+ # @param request_info [MicrosoftKiotaAbstractions::RequestInformation]
342
+ # @return [Hash] +{}+ on an empty 2xx, or the decoded body when one is present
343
+ # @api private
344
+ def self.void_request!(adapter, request_info)
345
+ # Replicate the first half of the adapter's send_async by hand — apply the
346
+ # base URL, run the bearer-auth Fiber (which mutates request_info's headers),
347
+ # then build the native Faraday request. We deliberately do NOT call the
348
+ # adapter's convert_to_native_request_async helper: its body uses a bare
349
+ # +return+ inside a Fiber, which raises LocalJumpError("unexpected return")
350
+ # when resumed under Ruby 3.x.
351
+ request_info.path_parameters["baseurl"] = adapter.get_base_url
352
+ # authenticate_request returns a Fiber only when the Authorization header is
353
+ # not already present; it returns nil otherwise. Guard the resume.
354
+ auth_fiber = adapter.authentication_provider.authenticate_request(request_info)
355
+ auth_fiber.resume if auth_fiber.respond_to?(:resume)
356
+ request = adapter.get_request_from_request_info(request_info)
357
+ response = adapter.client.run_request(
358
+ request.http_method, request.path, request.body, request.headers
359
+ )
360
+
361
+ status = response.status
362
+ if status >= 400
363
+ # Reuse the typed-error translation: synthesize a message carrying the
364
+ # ":<status>" token raise_translated keys off, plus any body text.
365
+ body = response.body.to_s
366
+ err = MicrosoftKiotaAbstractions::ApiError.new(
367
+ "The server returned an unexpected status code:#{status}" \
368
+ "#{body.empty? ? '' : " #{body}"}"
369
+ )
370
+ Ghostcrawl.raise_translated(err)
371
+ end
372
+
373
+ body = response.body.to_s
374
+ return {} if body.strip.empty?
375
+
376
+ parsed = (JSON.parse(body) rescue nil)
377
+ parsed.is_a?(Hash) ? parsed.transform_values { |v| to_hash(v) } : {}
378
+ rescue Ghostcrawl::GhostcrawlError
379
+ raise
380
+ rescue StandardError => e
381
+ Ghostcrawl.raise_translated(e)
382
+ end
383
+
204
384
  # Inspects a decoded HTTP-200 response hash for a RESULT-channel failure (the
205
385
  # target page could not be scraped) and raises {Ghostcrawl::ScrapeError} when
206
386
  # one is present. This is the reliable, highest-value error path: the body is
@@ -283,26 +463,101 @@ module Ghostcrawl
283
463
 
284
464
  # Manage crawl runs — /v1/crawl-runs.
285
465
  class CrawlRunsClient
286
- def initialize(v1)
466
+ # Terminal run states — a crawl run in any of these will never change again,
467
+ # so a wait can stop. Both spellings of "cancelled" are accepted.
468
+ TERMINAL_STATUSES = %w[completed failed cancelled canceled].freeze
469
+
470
+ # Per-request server-block window (seconds) for the long-poll wait. Each
471
+ # +GET ...?wait=true&timeout_s=WAIT_WINDOW_S+ makes the SERVER block for up
472
+ # to this long, so the client never sleeps between checks. Kept well under
473
+ # the client read timeout ({Client::DEFAULT_TIMEOUT}) so a single blocking
474
+ # request can never trip the transport timeout.
475
+ WAIT_WINDOW_S = 30
476
+
477
+ def initialize(v1, adapter = nil)
287
478
  @v1 = v1
479
+ @adapter = adapter
288
480
  end
289
481
 
290
482
  # Start a new crawl run from a seed URL.
291
483
  # Delegates to POST /v1/crawl-runs via the generated CrawlRunsRequestBuilder.
292
484
  # The endpoint is a tagged union: a start request requires +action: "start"+
293
485
  # and a +seed_urls+ array (not a bare +url+).
486
+ #
487
+ # Pass +wait: true+ to block until the run reaches a terminal state
488
+ # (completed | failed | cancelled) or +timeout+ seconds elapse. This sends
489
+ # +wait_until: "completed"+ so the SERVER blocks — no client-side poll loop,
490
+ # no +sleep+. When the run is still running at +timeout+ the current
491
+ # (non-terminal) record is returned; call {#wait_for_completion} again to
492
+ # keep waiting.
493
+ #
294
494
  # @param url [String] seed URL
295
495
  # @param max_depth [Integer] maximum crawl depth (default 2)
296
496
  # @param max_pages [Integer] maximum pages (default 100)
497
+ # @param wait [Boolean] block until the run is terminal (default false)
498
+ # @param timeout [Integer] total seconds to wait when +wait: true+ (default 300)
297
499
  # @param raise_on_result_error [Boolean] raise {Ghostcrawl::ScrapeError} on a
298
- # target-side (HTTP-200) failure instead of returning the raw hash (default true)
299
- # @return [Hash] crawl run record with +run_id+ and +status+
300
- def start(url:, max_depth: 2, max_pages: 100, raise_on_result_error: true, **opts)
301
- body = AdditionalDataBody.new({ "action" => "start", "seed_urls" => [url],
302
- "max_depth" => max_depth, "max_pages" => max_pages }
303
- .merge(opts.transform_keys(&:to_s)))
304
- hash = ResponseHelper.to_hash(@v1.crawl_runs.post(body))
305
- raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
500
+ # target-side (HTTP-200) failure instead of returning the raw hash (default true).
501
+ # Ignored on the +wait: true+ path, which always returns the terminal run so
502
+ # the caller can inspect a +failed+ status.
503
+ # @return [Hash] crawl run record with +run_id+ and +status+ (results present
504
+ # when it completed while waiting)
505
+ def start(url:, max_depth: 2, max_pages: 100, wait: false, timeout: 300,
506
+ raise_on_result_error: true, **opts)
507
+ data = { "action" => "start", "seed_urls" => [url],
508
+ "max_depth" => max_depth, "max_pages" => max_pages }
509
+ .merge(opts.transform_keys(&:to_s))
510
+
511
+ unless wait
512
+ hash = ResponseHelper.to_hash(@v1.crawl_runs.post(AdditionalDataBody.new(data)))
513
+ return raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
514
+ end
515
+
516
+ # Start-and-wait: ask the server to block until terminal. Bound the first
517
+ # server block to a safe window, then long-poll the rest via GET so a low
518
+ # per-request read timeout can never abort the whole wait.
519
+ window = [timeout, WAIT_WINDOW_S].min
520
+ data["wait_until"] = "completed"
521
+ data["timeout_s"] = window
522
+ hash = ResponseHelper.to_hash(@v1.crawl_runs.post(AdditionalDataBody.new(data)))
523
+ return hash if terminal?(hash)
524
+
525
+ run_id = hash["run_id"]
526
+ remaining = timeout - window
527
+ return hash if run_id.nil? || run_id.to_s.empty? || remaining <= 0
528
+ wait_for_completion(run_id, timeout: remaining)
529
+ end
530
+
531
+ # Block until an existing crawl run reaches a terminal state, or +timeout+
532
+ # seconds elapse. Event-driven: each iteration issues a SERVER-blocking
533
+ # +GET /v1/crawl-runs/{run_id}?wait=true&timeout_s=N+ that returns the moment
534
+ # the run goes terminal (or after its own window). There is no client +sleep+
535
+ # — the wait cost lives on the server, and successive blocking windows are
536
+ # chained until the caller's +timeout+ deadline.
537
+ #
538
+ # On timeout the current non-terminal run is returned (never raises for a
539
+ # slow run); a terminal run is returned as soon as it is observed.
540
+ #
541
+ # @param run_id [String] the run to wait on
542
+ # @param timeout [Integer] total seconds to wait (default 300)
543
+ # @return [Hash] the run record (terminal when it finished in time)
544
+ def wait_for_completion(run_id, timeout: 300)
545
+ if run_id.nil? || run_id.to_s.empty?
546
+ raise ArgumentError, "run_id is required"
547
+ end
548
+
549
+ deadline = monotonic + timeout
550
+ loop do
551
+ remaining = deadline - monotonic
552
+ # Deadline reached — one final non-blocking read of the current state.
553
+ return get(run_id) if remaining <= 0
554
+
555
+ window = [remaining, WAIT_WINDOW_S].min
556
+ run = fetch_waiting(run_id, window)
557
+ return run if terminal?(run)
558
+ # The server just blocked for ~window seconds; loop straight into the
559
+ # next blocking window. No client-side delay.
560
+ end
306
561
  end
307
562
 
308
563
  # List crawl runs.
@@ -322,6 +577,33 @@ module Ghostcrawl
322
577
  def cancel(run_id)
323
578
  ResponseHelper.to_hash(@v1.crawl_runs.by_run_id(run_id).cancel.post)
324
579
  end
580
+
581
+ private
582
+
583
+ # True when +run+ is a Hash carrying a terminal +status+.
584
+ def terminal?(run)
585
+ run.is_a?(Hash) && TERMINAL_STATUSES.include?(run["status"].to_s)
586
+ end
587
+
588
+ # Issues one SERVER-blocking GET on the run with +?wait=true&timeout_s=N+.
589
+ #
590
+ # The generated item builder's +to_get_request_information+ does not wire
591
+ # query parameters (its URL template has no query placeholder), so we take
592
+ # the request it builds, extend the template with +{?wait,timeout_s}+, and
593
+ # set the params before sending through the raw adapter — mirroring how
594
+ # {Client#me} re-issues a request with the Binary (raw-JSON) response factory.
595
+ def fetch_waiting(run_id, timeout_s)
596
+ request_info = @v1.crawl_runs.by_run_id(run_id).to_get_request_information(nil)
597
+ request_info.url_template = "{+baseurl}/v1/crawl-runs/{run_id}{?wait,timeout_s}"
598
+ request_info.query_parameters["wait"] = "true"
599
+ request_info.query_parameters["timeout_s"] = timeout_s.to_i.to_s
600
+ ResponseHelper.to_hash(@adapter.send_async(request_info, Ghostcrawl::V1::Binary, {}))
601
+ end
602
+
603
+ # Monotonic clock (seconds) — immune to wall-clock adjustments during a wait.
604
+ def monotonic
605
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
606
+ end
325
607
  end
326
608
 
327
609
  # Manage browser sessions — /v1/sessions.
@@ -347,7 +629,7 @@ module Ghostcrawl
347
629
  # Extend a session's TTL.
348
630
  # Delegates to POST /v1/sessions/{id}/extend via the generated builder.
349
631
  def extend(session_id, duration_seconds: 300)
350
- ResponseHelper.to_hash(@v1.sessions.by_profile__id(session_id).extend.post(
632
+ ResponseHelper.to_hash(@v1.sessions.by_profile_id(session_id).extend.post(
351
633
  AdditionalDataBody.new({ "ttl_seconds" => duration_seconds })
352
634
  ))
353
635
  end
@@ -355,14 +637,15 @@ module Ghostcrawl
355
637
  # Release a session back to the pool.
356
638
  # Delegates to POST /v1/sessions/{id}/release via the generated builder.
357
639
  def release(session_id)
358
- ResponseHelper.to_hash(@v1.sessions.by_profile__id(session_id).release.post)
640
+ ResponseHelper.to_hash(@v1.sessions.by_profile_id(session_id).release.post)
359
641
  end
360
642
  end
361
643
 
362
644
  # Manage identity profiles — /v1/profiles.
363
645
  class ProfilesClient
364
- def initialize(v1)
646
+ def initialize(v1, adapter = nil)
365
647
  @v1 = v1
648
+ @adapter = adapter
366
649
  end
367
650
 
368
651
  # List all profiles.
@@ -392,15 +675,18 @@ module Ghostcrawl
392
675
 
393
676
  # Delete a profile.
394
677
  # Delegates to DELETE /v1/profiles/{name} via the generated builder.
678
+ # The endpoint answers 204 No Content; routing through {ResponseHelper.void_request!}
679
+ # avoids the Kiota JSON parser choking on the empty body.
395
680
  def delete(name)
396
- ResponseHelper.to_hash(@v1.profiles.by_name(name).delete)
681
+ ResponseHelper.void_request!(@adapter, @v1.profiles.by_name(name).to_delete_request_information(nil))
397
682
  end
398
683
  end
399
684
 
400
685
  # Manage webhooks — /v1/webhooks.
401
686
  class WebhooksClient
402
- def initialize(v1)
687
+ def initialize(v1, adapter = nil)
403
688
  @v1 = v1
689
+ @adapter = adapter
404
690
  end
405
691
 
406
692
  # List all webhooks.
@@ -426,10 +712,13 @@ module Ghostcrawl
426
712
  end
427
713
 
428
714
  # Delete a webhook.
429
- # Delegates to DELETE /v1/webhooks/{id} via the generated builder.
715
+ # Delegates to DELETE /v1/webhooks/{id}. The generated builder passes a +nil+
716
+ # response factory (204 void), which the Kiota adapter rejects with a bare
717
+ # StandardError BEFORE sending the request — so we build the request info and
718
+ # run it through {ResponseHelper.void_request!} instead, which actually fires
719
+ # the DELETE and returns +{}+ on success.
430
720
  def delete(webhook_id)
431
- @v1.webhooks.by_webhook_id(webhook_id).delete
432
- {}
721
+ ResponseHelper.void_request!(@adapter, @v1.webhooks.by_webhook_id(webhook_id).to_delete_request_information(nil))
433
722
  end
434
723
 
435
724
  # Rotate the signing secret for a webhook.
@@ -441,8 +730,9 @@ module Ghostcrawl
441
730
 
442
731
  # Manage schedules — /v1/schedules.
443
732
  class SchedulesClient
444
- def initialize(v1)
733
+ def initialize(v1, adapter = nil)
445
734
  @v1 = v1
735
+ @adapter = adapter
446
736
  end
447
737
 
448
738
  # List all schedules.
@@ -459,16 +749,45 @@ module Ghostcrawl
459
749
 
460
750
  # Create a new schedule.
461
751
  # Delegates to POST /v1/schedules via the generated SchedulesRequestBuilder.
462
- def create(cron:, task:, **opts)
463
- data = { "cron_expr" => cron, "task" => task }.merge(opts.transform_keys(&:to_s))
752
+ #
753
+ # The API's ScheduleCreateRequest requires +name+, +job_type+
754
+ # ("scrape" | "crawl" | "change_monitor"), +cron_expr+, and +job_params+
755
+ # (the full scrape/crawl request body). It rejects any other top-level field
756
+ # with a 422 — so a legacy +task+ key must be TRANSLATED, never forwarded.
757
+ #
758
+ # @param cron [String] cron expression (sent as +cron_expr+)
759
+ # @param name [String] schedule name (required by the API)
760
+ # @param job_type [String, nil] "scrape" | "crawl" | "change_monitor"
761
+ # @param job_params [Hash, nil] full job request body (e.g. { url: ... })
762
+ # @param task [Hash, nil] DEPRECATED legacy shape { "action" => ..., ...rest };
763
+ # when given (and job_type/job_params are absent) it is split into
764
+ # +job_type+ (from +task["action"]+) and +job_params+ (the remaining keys).
765
+ def create(cron:, name: nil, job_type: nil, job_params: nil, task: nil, **opts)
766
+ # Back-compat: derive job_type/job_params from a legacy `task` hash.
767
+ if task.is_a?(Hash)
768
+ t = task.transform_keys(&:to_s)
769
+ job_type ||= t["action"] || t["job_type"] || t["type"]
770
+ job_params ||= t.reject { |k, _| %w[action job_type type].include?(k) }
771
+ end
772
+
773
+ data = { "cron_expr" => cron }
774
+ data["name"] = name unless name.nil?
775
+ data["job_type"] = job_type unless job_type.nil?
776
+ data["job_params"] = job_params unless job_params.nil?
777
+ # opts may override/supply name/job_type/job_params/notify_webhook/monitor_mode.
778
+ # Never forward a raw `task` field — the API 422s on it.
779
+ data.merge!(opts.transform_keys(&:to_s).reject { |k, _| k == "task" })
780
+
464
781
  ResponseHelper.to_hash(@v1.schedules.post(AdditionalDataBody.new(data)))
465
782
  end
466
783
 
467
784
  # Delete a schedule.
468
- # Delegates to DELETE /v1/schedules/{id} via the generated builder.
785
+ # Delegates to DELETE /v1/schedules/{id}. The generated builder returns a Fiber
786
+ # that must be resumed to actually send the request (the old +; {}+ discarded it,
787
+ # so the DELETE never fired). {ResponseHelper.void_request!} runs it and tolerates
788
+ # the empty 204 body.
469
789
  def delete(schedule_id)
470
- @v1.schedules.by_schedule_id(schedule_id).delete
471
- {}
790
+ ResponseHelper.void_request!(@adapter, @v1.schedules.by_schedule_id(schedule_id).to_delete_request_information(nil))
472
791
  end
473
792
  end
474
793
 
@@ -519,8 +838,9 @@ module Ghostcrawl
519
838
 
520
839
  # Manage session recordings — /v1/recordings.
521
840
  class RecordingsClient
522
- def initialize(v1)
841
+ def initialize(v1, adapter = nil)
523
842
  @v1 = v1
843
+ @adapter = adapter
524
844
  end
525
845
 
526
846
  # List all recordings.
@@ -531,15 +851,17 @@ module Ghostcrawl
531
851
 
532
852
  # Get a recording by ID.
533
853
  # Delegates to GET /v1/recordings/{id} via the generated builder.
854
+ # (The generated accessor is +by_recording_id+, single underscore.)
534
855
  def get(recording_id)
535
- ResponseHelper.to_hash(@v1.recordings.by_recording__id(recording_id).get)
856
+ ResponseHelper.to_hash(@v1.recordings.by_recording_id(recording_id).get)
536
857
  end
537
858
 
538
859
  # Delete a recording.
539
- # Delegates to DELETE /v1/recordings/{id} via the generated builder.
860
+ # Delegates to DELETE /v1/recordings/{id}. Same void-DELETE defect as webhooks
861
+ # (generated builder passes a +nil+ factory → adapter raises before sending), so
862
+ # we route through {ResponseHelper.void_request!}.
540
863
  def delete(recording_id)
541
- @v1.recordings.by_recording__id(recording_id).delete
542
- {}
864
+ ResponseHelper.void_request!(@adapter, @v1.recordings.by_recording_id(recording_id).to_delete_request_information(nil))
543
865
  end
544
866
  end
545
867
 
@@ -628,13 +950,18 @@ module Ghostcrawl
628
950
  # (Binary) response factory instead of the typed MeResponse model.
629
951
  @adapter = adapter
630
952
 
631
- @crawl_runs = CrawlRunsClient.new(@v1)
953
+ # CrawlRunsClient needs the raw adapter for the event-driven long-poll wait
954
+ # (the generated item builder can't attach ?wait/&timeout_s query params).
955
+ @crawl_runs = CrawlRunsClient.new(@v1, @adapter)
632
956
  @sessions = SessionsClient.new(@v1)
633
- @profiles = ProfilesClient.new(@v1)
634
- @webhooks = WebhooksClient.new(@v1)
635
- @schedules = SchedulesClient.new(@v1)
957
+ # Sub-clients with void-DELETE endpoints (204 No Content) also need the raw
958
+ # adapter so ResponseHelper.void_request! can work around the broken generated
959
+ # delete builders. See ResponseHelper.void_request! for the two Kiota defects.
960
+ @profiles = ProfilesClient.new(@v1, @adapter)
961
+ @webhooks = WebhooksClient.new(@v1, @adapter)
962
+ @schedules = SchedulesClient.new(@v1, @adapter)
636
963
  @datasets = DatasetsClient.new(@v1)
637
- @recordings = RecordingsClient.new(@v1)
964
+ @recordings = RecordingsClient.new(@v1, @adapter)
638
965
  @kv = KVClient.new(@v1)
639
966
  end
640
967
 
@@ -722,17 +1049,34 @@ module Ghostcrawl
722
1049
 
723
1050
  # Start a deep crawl from a seed URL.
724
1051
  # Delegates to POST /v1/crawl/deep via the generated CrawlDeepRequestBuilder.
1052
+ #
1053
+ # Pass +wait: true+ to block until the run is terminal. The deep-crawl start
1054
+ # returns a +run_id+, then the wait is handled by the same event-driven
1055
+ # server-blocking long-poll as {CrawlRunsClient#wait_for_completion} — no
1056
+ # client-side +sleep+ loop.
1057
+ #
725
1058
  # @param url [String] seed URL
726
1059
  # @param max_depth [Integer] maximum crawl depth (default 2)
727
1060
  # @param max_pages [Integer] maximum pages (default 100)
1061
+ # @param wait [Boolean] block until the run is terminal (default false)
1062
+ # @param timeout [Integer] total seconds to wait when +wait: true+ (default 300)
728
1063
  # @param raise_on_result_error [Boolean] raise {Ghostcrawl::ScrapeError} on a
729
- # target-side (HTTP-200) failure instead of returning the raw hash (default true)
730
- # @return [Hash] crawl run record
731
- def crawl(url:, max_depth: 2, max_pages: 100, raise_on_result_error: true, **opts)
1064
+ # target-side (HTTP-200) failure instead of returning the raw hash (default true).
1065
+ # Ignored on the +wait: true+ path, which returns the terminal run.
1066
+ # @return [Hash] crawl run record (terminal when +wait: true+ and it finished in time)
1067
+ def crawl(url:, max_depth: 2, max_pages: 100, wait: false, timeout: 300,
1068
+ raise_on_result_error: true, **opts)
732
1069
  data = { "seed_urls" => [url], "max_depth" => max_depth,
733
1070
  "max_urls" => max_pages }.merge(opts.transform_keys(&:to_s))
734
1071
  hash = ResponseHelper.to_hash(@v1.crawl.deep.post(AdditionalDataBody.new(data)))
735
- raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
1072
+
1073
+ unless wait
1074
+ return raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
1075
+ end
1076
+
1077
+ run_id = hash["run_id"]
1078
+ return hash if run_id.nil? || run_id.to_s.empty?
1079
+ @crawl_runs.wait_for_completion(run_id, timeout: timeout)
736
1080
  end
737
1081
 
738
1082
  # Map all URLs reachable from a seed URL.
@@ -763,14 +1107,22 @@ module Ghostcrawl
763
1107
 
764
1108
  # Normalizes a +"content"+ key onto a decoded scrape response.
765
1109
  #
766
- # The API returns the rendered page under a FORMAT-SPECIFIC key
767
- # (+"markdown"+, +"html"+, or +"text"+), but the documented quickstart reads
768
- # +result["content"]+. This mirrors that value onto +"content"+ in place,
769
- # KEEPING the format-specific key intact (backward compatible).
1110
+ # The API has two scrape response shapes:
1111
+ # * the FLAT +format="markdown"+ shape, which carries the rendered page at
1112
+ # the top level under the format-specific key (+"markdown"+/+"html"+/
1113
+ # +"text"+) but omits +identity_id+; and
1114
+ # * the standard ENVELOPE ({ +status+, +results+, +routing_mode+,
1115
+ # +request_class+, +identity_id+ }), where the rendered page lives under
1116
+ # +results[0]+ and the top-level +identity_id+ is present.
1117
+ # The documented quickstart reads +result["content"]+, so this mirrors the
1118
+ # rendered page onto +"content"+ in place from WHICHEVER shape is present,
1119
+ # KEEPING the original keys intact (backward compatible).
770
1120
  #
771
1121
  # No-op unless +result+ is a Hash that does not already carry +"content"+.
772
1122
  # The value chosen is: the field named by +result["format"]+ when that field
773
- # is a String, else the first String among +"markdown"+, +"html"+, +"text"+.
1123
+ # is a String, else the first String among +"markdown"+, +"html"+, +"text"+ —
1124
+ # looked up first at the top level (flat shape), then on +results[0]+
1125
+ # (envelope shape).
774
1126
  #
775
1127
  # @param result [Object] the decoded response (only mutated when a Hash)
776
1128
  # @return [Object] the same +result+, unchanged reference
@@ -779,21 +1131,34 @@ module Ghostcrawl
779
1131
  return result unless result.is_a?(Hash) && !result.key?("content")
780
1132
 
781
1133
  fmt = result["format"]
782
- value = result[fmt] if fmt.is_a?(String)
783
- value = nil unless value.is_a?(String)
1134
+ value = scrape_content_from(result, fmt)
784
1135
 
1136
+ # Envelope shape: the page is under results[0], not at the top level.
785
1137
  unless value.is_a?(String)
786
- %w[markdown html text].each do |key|
787
- candidate = result[key]
788
- if candidate.is_a?(String)
789
- value = candidate
790
- break
791
- end
792
- end
1138
+ first = result["results"]
1139
+ first = first.first if first.is_a?(Array)
1140
+ value = scrape_content_from(first, fmt) if first.is_a?(Hash)
793
1141
  end
794
1142
 
795
1143
  result["content"] = value if value.is_a?(String)
796
1144
  result
797
1145
  end
1146
+
1147
+ # Picks the rendered-page String out of a scrape result Hash: the field named
1148
+ # by +fmt+ when present, else the first String among +"markdown"+, +"html"+,
1149
+ # +"text"+. Returns +nil+ when +src+ carries no such String.
1150
+ # @api private
1151
+ def scrape_content_from(src, fmt)
1152
+ return nil unless src.is_a?(Hash)
1153
+
1154
+ value = src[fmt] if fmt.is_a?(String)
1155
+ return value if value.is_a?(String)
1156
+
1157
+ %w[markdown html text].each do |key|
1158
+ candidate = src[key]
1159
+ return candidate if candidate.is_a?(String)
1160
+ end
1161
+ nil
1162
+ end
798
1163
  end
799
1164
  end
@@ -92,7 +92,14 @@ module Ghostcrawl
92
92
  when 403 then FORBIDDEN
93
93
  when 404 then NOT_FOUND
94
94
  when 409 then CONFLICT
95
- when 422 then BYO_PROXY_INVALID
95
+ # 422 is the generic validation status the API returns for ANY malformed
96
+ # request (its canonical code is "bad_request": missing field, failed
97
+ # extraction validation, etc.). byo_proxy_invalid is only ONE 422 sub-case
98
+ # and, when it genuinely occurs, arrives WITH a problem+json body carrying
99
+ # code:"byo_proxy_invalid" (which raise_translated reads first). Using the
100
+ # BYO-specific code as the blanket 422 fallback mislabels every unrelated
101
+ # validation 422. Fall back to the generic bad_request instead.
102
+ when 422 then BAD_REQUEST
96
103
  when 429 then RATE_LIMITED
97
104
  when 503 then SERVICE_UNAVAILABLE
98
105
  when 504 then FLEET_TIMEOUT
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ghostcrawl
4
- VERSION = "2.2.2"
4
+ VERSION = "2.3.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ghostcrawl
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.2
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GhostCrawl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-01 00:00:00.000000000 Z
11
+ date: 2026-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: microsoft_kiota_abstractions
@@ -225,9 +225,6 @@ files:
225
225
  - _generated/v1/billing/usage/usage.rb
226
226
  - _generated/v1/billing/usage/usage_request_builder.rb
227
227
  - _generated/v1/binary/binary.rb
228
- - _generated/v1/binary/binary_request_builder.rb
229
- - _generated/v1/binary/download/download.rb
230
- - _generated/v1/binary/download/download_request_builder.rb
231
228
  - _generated/v1/budgets/budgets.rb
232
229
  - _generated/v1/budgets/budgets_request_builder.rb
233
230
  - _generated/v1/budgets/policy/item/item.rb
@@ -431,7 +428,7 @@ files:
431
428
  - lib/ghostcrawl/version.rb
432
429
  homepage: https://ghostcrawl.io
433
430
  licenses:
434
- - Nonstandard
431
+ - MIT
435
432
  metadata:
436
433
  homepage_uri: https://ghostcrawl.io
437
434
  documentation_uri: https://docs.ghostcrawl.io
@@ -1,31 +0,0 @@
1
- require 'microsoft_kiota_abstractions'
2
- require_relative '../../ghostcrawl'
3
- require_relative '../v1'
4
- require_relative './binary'
5
- require_relative './download/download_request_builder'
6
-
7
- module Ghostcrawl
8
- module V1
9
- module Binary
10
- ##
11
- # Builds and executes requests for operations under #v1#binary
12
- class BinaryRequestBuilder < MicrosoftKiotaAbstractions::BaseRequestBuilder
13
-
14
- ##
15
- # The download property
16
- def download()
17
- return Ghostcrawl::V1::Binary::Download::DownloadRequestBuilder.new(@path_parameters, @request_adapter)
18
- end
19
- ##
20
- ## Instantiates a new BinaryRequestBuilder and sets the default values.
21
- ## @param path_parameters Path parameters for the request
22
- ## @param request_adapter The request adapter to use to execute the requests.
23
- ## @return a void
24
- ##
25
- def initialize(path_parameters, request_adapter)
26
- super(path_parameters, request_adapter, "{+baseurl}/v1/binary")
27
- end
28
- end
29
- end
30
- end
31
- end
File without changes
@@ -1,77 +0,0 @@
1
- require 'microsoft_kiota_abstractions'
2
- require_relative '../../../ghostcrawl'
3
- require_relative '../../../models/h_t_t_p_validation_error'
4
- require_relative '../../v1'
5
- require_relative '../binary'
6
- require_relative './download'
7
-
8
- module Ghostcrawl
9
- module V1
10
- module Binary
11
- module Download
12
- ##
13
- # Builds and executes requests for operations under #v1#binary#download
14
- class DownloadRequestBuilder < MicrosoftKiotaAbstractions::BaseRequestBuilder
15
-
16
- ##
17
- ## Instantiates a new DownloadRequestBuilder and sets the default values.
18
- ## @param path_parameters Path parameters for the request
19
- ## @param request_adapter The request adapter to use to execute the requests.
20
- ## @return a void
21
- ##
22
- def initialize(path_parameters, request_adapter)
23
- super(path_parameters, request_adapter, "{+baseurl}/v1/binary/download?engine={engine}{&release_id*}")
24
- end
25
- ##
26
- ## Download your licensed engine binary (Chrome, Firefox, or WebKit) for self-hosting.Select the engine via the ``engine`` query parameter. The binary is streamedto the client and bound to your account.
27
- ## @param request_configuration Configuration for the request such as headers, query parameters, and middleware options.
28
- ## @return a Fiber of binary
29
- ##
30
- def get(request_configuration=nil)
31
- request_info = self.to_get_request_information(
32
- request_configuration
33
- )
34
- error_mapping = Hash.new
35
- error_mapping["422"] = lambda {|pn| Ghostcrawl::Models::HTTPValidationError.create_from_discriminator_value(pn) }
36
- return @request_adapter.send_async(request_info, Binary, error_mapping)
37
- end
38
- ##
39
- ## Download your licensed engine binary (Chrome, Firefox, or WebKit) for self-hosting.Select the engine via the ``engine`` query parameter. The binary is streamedto the client and bound to your account.
40
- ## @param request_configuration Configuration for the request such as headers, query parameters, and middleware options.
41
- ## @return a request_information
42
- ##
43
- def to_get_request_information(request_configuration=nil)
44
- request_info = MicrosoftKiotaAbstractions::RequestInformation.new()
45
- unless request_configuration.nil?
46
- request_info.add_headers_from_raw_object(request_configuration.headers)
47
- request_info.set_query_string_parameters_from_raw_object(request_configuration.query_parameters)
48
- request_info.add_request_options(request_configuration.options)
49
- end
50
- request_info.url_template = @url_template
51
- request_info.path_parameters = @path_parameters
52
- request_info.http_method = :GET
53
- request_info.headers.try_add('Accept', 'application/json')
54
- return request_info
55
- end
56
- ##
57
- ## Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
58
- ## @param raw_url The raw URL to use for the request builder.
59
- ## @return a download_request_builder
60
- ##
61
- def with_url(raw_url)
62
- raise StandardError, 'raw_url cannot be null' if raw_url.nil?
63
- return DownloadRequestBuilder.new(raw_url, @request_adapter)
64
- end
65
-
66
- ##
67
- # Download your licensed engine binary (Chrome, Firefox, or WebKit) for self-hosting.Select the engine via the ``engine`` query parameter. The binary is streamedto the client and bound to your account.
68
- class DownloadRequestBuilderGetQueryParameters
69
-
70
- attr_accessor :engine
71
- attr_accessor :release_id
72
- end
73
- end
74
- end
75
- end
76
- end
77
- end