/
cassette_class.R
759 lines (700 loc) · 28.4 KB
/
cassette_class.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
#' @title Cassette handler
#' @description Main R6 class that is called from the main user facing
#' function [use_cassette()]
#' @export
#' @keywords internal
#' @return an object of class `Cassette`
#' @seealso [vcr_configure()], [use_cassette()], [insert_cassette()]
#' @section Points of webmockr integration:
#' - `initialize()`: webmockr is used in the `initialize()` method to
#' create webmockr stubs. stubs are created on call to `Cassette$new()`
#' within `insert_cassette()`, but then on exiting `use_cassette()`,
#' or calling `eject()` on `Cassette` class from `insert_cassette()`,
#' stubs are cleaned up.
#' - `eject()` method: [webmockr::disable()] is called before exiting
#' eject to disable webmock so that webmockr does not affect any HTTP
#' requests that happen afterwards
#' - `call_block()` method: call_block is used in the [use_cassette()]
#' function to evaluate whatever code is passed to it; within call_block
#' [webmockr::webmockr_allow_net_connect()] is run before we evaluate
#' the code block to allow real HTTP requests, then
#' [webmockr::webmockr_disable_net_connect()] is called after evalulating
#' the code block to disallow real HTTP requests
#' - `make_http_interaction()` method: [webmockr::pluck_body()] utility
#' function is used to pull the request body out of the HTTP request
#' - `serialize_to_crul()` method: method: [webmockr::RequestSignature] and
#' [webmockr::Response] are used to build a request and response,
#' respectively, then passed to [webmockr::build_crul_response()]
#' to make a complete `crul` HTTP response object
#' @examples \dontrun{
#' library(vcr)
#' vcr_configure(dir = tempdir())
#'
#' res <- Cassette$new(name = "bob")
#' res$file()
#' res$originally_recorded_at()
#' res$recording()
#' res$serializable_hash()
#' res$eject()
#' res$should_remove_matching_existing_interactions()
#' res$storage_key()
#' res$match_requests_on
#'
#' # record all requests
#' res <- Cassette$new("foobar", record = "all")
#' res$eject()
#'
#' # cleanup
#' unlink(file.path(tempdir(), c("bob.yml", "foobar.yml")))
#'
#' library(vcr)
#' vcr_configure(dir = tempdir())
#' res <- Cassette$new(name = "jane")
#' library(crul)
#' # HttpClient$new("https://hb.opencpu.org")$get("get")
#' }
Cassette <- R6::R6Class(
"Cassette",
public = list(
#' @field name (character) cassette name
name = NA,
#' @field record (character) record mode
record = "all",
#' @field manfile (character) cassette file path
manfile = NA,
#' @field recorded_at (character) date/time recorded at
recorded_at = NA,
#' @field serialize_with (character) serializer to use (yaml|json)
serialize_with = "yaml",
#' @field serializer (character) serializer to use (yaml|json)
serializer = NA,
#' @field persist_with (character) persister to use (FileSystem only)
persist_with = "FileSystem",
#' @field persister (character) persister to use (FileSystem only)
persister = NA,
#' @field match_requests_on (character) matchers to use
#' default: method & uri
match_requests_on = c("method", "uri"),
#' @field re_record_interval (numeric) the re-record interval
re_record_interval = NULL,
#' @field tag ignored, not used right now
tag = NA,
#' @field tags ignored, not used right now
tags = NA,
#' @field root_dir root dir, gathered from [vcr_configuration()]
root_dir = NA,
#' @field update_content_length_header (logical) Whether to overwrite the
#' `Content-Length` header
update_content_length_header = FALSE,
#' @field allow_playback_repeats (logical) Whether to allow a single HTTP
#' interaction to be played back multiple times
allow_playback_repeats = FALSE,
#' @field allow_unused_http_interactions (logical) ignored, not used right now
allow_unused_http_interactions = TRUE,
#' @field exclusive (logical) ignored, not used right now
exclusive = FALSE,
#' @field preserve_exact_body_bytes (logical) Whether to base64 encode the
#' bytes of the requests and responses
preserve_exact_body_bytes = FALSE,
#' @field args (list) internal use
args = list(),
#' @field http_interactions_ (list) internal use
http_interactions_ = NULL,
#' @field new_recorded_interactions (list) internal use
new_recorded_interactions = NULL,
#' @field clean_outdated_http_interactions (logical) Should outdated interactions
#' be recorded back to file
clean_outdated_http_interactions = FALSE,
#' @field to_return (logical) internal use
to_return = NULL,
#' @field cassette_opts (list) various cassette options
cassette_opts = NULL,
#' @description Create a new `Cassette` object
#' @param name The name of the cassette. vcr will sanitize this to ensure it
#' is a valid file name.
#' @param record The record mode. Default: "once". In the future we'll support
#' "once", "all", "none", "new_episodes". See [recording] for more information
#' @param serialize_with (character) Which serializer to use.
#' Valid values are "yaml" (default), the only one supported for now.
#' @param persist_with (character) Which cassette persister to
#' use. Default: "file_system". You can also register and use a
#' custom persister.
#' @param match_requests_on List of request matchers
#' to use to determine what recorded HTTP interaction to replay. Defaults to
#' `["method", "uri"]`. The built-in matchers are "method", "uri",
#' "headers" and "body" ("host" and "path" not supported yet, but should
#' be in a future version)
#' @param re_record_interval (numeric) When given, the cassette will be
#' re-recorded at the given interval, in seconds.
#' @param tag,tags tags ignored, not used right now
#' @param update_content_length_header (logical) Whether or
#' not to overwrite the `Content-Length` header of the responses to
#' match the length of the response body. Default: `FALSE`
#' @param allow_playback_repeats (logical) Whether or not to
#' allow a single HTTP interaction to be played back multiple times.
#' Default: `FALSE`.
#' @param allow_unused_http_interactions (logical) ignored, not used right now
#' @param exclusive (logical) ignored, not used right now
#' @param preserve_exact_body_bytes (logical) Whether or not
#' to base64 encode the bytes of the requests and responses for
#' this cassette when serializing it. See also `preserve_exact_body_bytes`
#' in [vcr_configure()]. Default: `FALSE`
#' @param clean_outdated_http_interactions (logical) Should outdated interactions
#' be recorded back to file. Default: `FALSE`
#' @return A new `Cassette` object
initialize = function(
name, record, serialize_with, persist_with, match_requests_on,
re_record_interval, tag, tags, update_content_length_header,
allow_playback_repeats, allow_unused_http_interactions,
exclusive, preserve_exact_body_bytes,
clean_outdated_http_interactions) {
self$name <- name
self$root_dir <- vcr_configuration()$dir
self$serialize_with <- serialize_with %||% vcr_c$serialize_with
check_serializer(self$serialize_with)
self$persist_with <- persist_with %||% vcr_c$persist_with
if (!missing(record)) {
self$record <- check_record_mode(record)
}
self$make_dir()
ext <- switch(self$serialize_with, yaml = "yml", json = "json")
self$manfile <- sprintf("%s/%s.%s", path.expand(cassette_path()),
self$name, ext)
if (!file.exists(self$manfile)) cat("\n", file = self$manfile)
if (!missing(match_requests_on)) {
self$match_requests_on <- check_request_matchers(match_requests_on)
}
if (!missing(re_record_interval))
self$re_record_interval <- re_record_interval
if (!missing(tag)) self$tag = tag
if (!missing(tags)) self$tags = tags
if (!missing(update_content_length_header)) {
assert(update_content_length_header, "logical")
self$update_content_length_header = update_content_length_header
}
if (!missing(allow_playback_repeats)) {
assert(allow_playback_repeats, "logical")
self$allow_playback_repeats = allow_playback_repeats
}
if (!missing(allow_unused_http_interactions))
self$allow_unused_http_interactions = allow_unused_http_interactions
if (!missing(exclusive)) self$exclusive = exclusive
if (!missing(preserve_exact_body_bytes)) {
assert(preserve_exact_body_bytes, "logical")
self$preserve_exact_body_bytes <- preserve_exact_body_bytes
}
if (!missing(clean_outdated_http_interactions)) {
self$clean_outdated_http_interactions <- clean_outdated_http_interactions
}
self$make_args()
if (!file.exists(self$manfile)) self$write_metadata()
self$recorded_at <- file.info(self$file())$mtime
self$serializer = serializer_fetch(self$serialize_with, self$name)
self$persister = persister_fetch(self$persist_with, self$serializer$path)
# check for re-record
if (self$should_re_record()) self$record <- "all"
# get previously recorded interactions
## if none pass, if some found, make webmockr stubs
#### first, get previously recorded interactions into `http_interactions_` var
self$http_interactions()
# then do the rest
prev <- self$previously_recorded_interactions()
if (length(prev) > 0) {
stub_previous_request <- function(previous_interaction) {
req <- previous_interaction$request
res <- previous_interaction$response
uripp <- crul::url_parse(req$uri)
m <- self$match_requests_on
.stub_request_with <- function(match_parameters, request) {
.check_match_parameters <- function(mp) {
vmp <- c("method", "uri", "body", "headers", "query")
mp[mp %in% vmp]
}
mp <- .check_match_parameters(match_parameters)
stub_method <- ifelse("method" %in% mp,
req$method,
"any"
)
stub_uri <- ifelse(identical(mp, c("body")),
".+",
ifelse("uri" %in% mp,
req$uri,
"."
)
)
if (stub_uri %in% c(".", ".+")) {
sr <- webmockr::stub_request(method = stub_method,
uri_regex = stub_uri)
} else {
sr <- webmockr::stub_request(method = stub_method,
uri = stub_uri)
}
with_list <- list()
if ("query" %in% mp) {
with_list[["query"]] <- uripp$parameter
}
if ("headers" %in% mp) {
with_list[["headers"]] <- req$headers
}
if ("body" %in% mp) {
with_list[["body"]] <- req$body
}
# if list is empty, skip wi_th
if (length(with_list) != 0) webmockr::wi_th(sr, .list = with_list)
}
.stub_request_with(m, req)
}
invisible(lapply(prev, stub_previous_request))
}
tmp <- list(
self$name,
self$record,
self$serialize_with,
self$persist_with,
self$match_requests_on,
self$update_content_length_header,
self$allow_playback_repeats,
self$preserve_exact_body_bytes
)
init_opts <- compact(
stats::setNames(tmp, c("name", "record", "serialize_with",
"persist_with", "match_requests_on", "update_content_length_header",
"allow_playback_repeats", "preserve_exact_body_bytes")))
self$cassette_opts <- init_opts
init_opts <- paste(names(init_opts), unname(init_opts), sep = ": ",
collapse = ", ")
vcr_log_info(sprintf("Initialized with options: {%s}", init_opts),
vcr_c$log_opts$date)
# create new env for recorded interactions
self$new_recorded_interactions <- list()
# check on write to disk path
if (!is.null(vcr_c$write_disk_path))
dir.create(vcr_c$write_disk_path, showWarnings = FALSE, recursive = TRUE)
# put cassette in vcr_cassettes environment
include_cassette(self)
},
#' @description print method for `Cassette` objects
#' @param x self
#' @param ... ignored
print = function(x, ...) {
cat(paste0("<vcr - Cassette> ", self$name), sep = "\n")
cat(paste0(" Record method: ", self$record), sep = "\n")
cat(paste0(" Serialize with: ", self$serialize_with), sep = "\n")
cat(paste0(" Persist with: ", self$persist_with), sep = "\n")
cat(paste0(" Re-record interval (s): ", self$re_record_interval),
sep = "\n")
cat(paste0(" Clean outdated interactions?: ",
self$clean_outdated_http_interactions), sep = "\n")
cat(paste0(" update_content_length_header: ",
self$update_content_length_header), sep = "\n")
cat(paste0(" allow_playback_repeats: ",
self$allow_playback_repeats), sep = "\n")
cat(paste0(" allow_unused_http_interactions: ",
self$allow_unused_http_interactions), sep = "\n")
cat(paste0(" exclusive: ", self$exclusive), sep = "\n")
cat(paste0(" preserve_exact_body_bytes: ",
self$preserve_exact_body_bytes), sep = "\n")
invisible(self)
},
#' @description run code
#' @param ... pass in things to be evaluated
#' @return various
call_block = function(...) {
tmp <- list(...)
if (length(tmp) == 0) {
stop("`vcr::use_cassette` requires a code block. ",
"If you cannot wrap your code in a block, use ",
"`vcr::insert_cassette` / `vcr::eject_cassette` instead")
}
invisible(force(...))
},
#' @description ejects the current cassette
#' @return self
eject = function() {
on.exit(private$remove_empty_cassette())
self$write_recorded_interactions_to_disk()
# remove cassette from list of current cassettes
rm(list = self$name, envir = vcr_cassettes)
if (!vcr_c$quiet) message("ejecting cassette: ", self$name)
# disable webmockr
webmockr::disable(quiet=vcr_c$quiet)
# set current casette name to NULL
vcr__env$current_cassette <- NULL
# return self
return(self)
},
#' @description get the file path for the cassette
#' @return character
file = function() self$manfile,
#' @description is the cassette in recording mode?
#' @return logical
recording = function() {
if (self$record == "none") {
return(FALSE)
} else if (self$record == "once") {
return(self$is_empty())
} else {
return(TRUE)
}
},
#' @description is the cassette on disk empty
#' @return logical
is_empty = function() {
nchar(self$raw_cassette_bytes()) < 1
},
#' @description timestamp the cassette was originally recorded at
#' @return POSIXct date
originally_recorded_at = function() {
as.POSIXct(self$recorded_at, tz = "GMT")
},
#' @description Get a list of the http interactions to record + recorded_with
#' @return list
serializable_hash = function() {
list(
http_interactions = self$interactions_to_record(),
recorded_with = utils::packageVersion("vcr")
)
},
#' @description Get the list of http interactions to record
#' @return list
interactions_to_record = function() {
## FIXME - gotta sort out defining and using hooks better
## just returning exact same input
self$merged_interactions()
# FIXME: not sure what's going on here, so not using yet
#. maybe we don't need this?
# "We dee-dupe the interactions by roundtripping them to/from a hash.
# This is necessary because `before_record` can mutate the interactions."
# lapply(self$merged_interactions(), function(z) {
# VCRHooks$invoke_hook("before_record", z)
# })
},
#' @description Get interactions to record
#' @return list
merged_interactions = function() {
old_interactions <- self$previously_recorded_interactions()
old_interactions <- lapply(old_interactions, function(x) {
HTTPInteraction$new(
request = x$request,
response = x$response,
recorded_at = x$recorded_at)
})
if (self$should_remove_matching_existing_interactions()) {
new_interaction_list <-
HTTPInteractionList$new(self$new_recorded_interactions,
self$match_requests_on)
old_interactions <-
Filter(function(x) {
req <- Request$new()$from_hash(x$request)
!unlist(new_interaction_list$has_interaction_matching(req))
},
old_interactions
)
}
return(c(self$up_to_date_interactions(old_interactions),
self$new_recorded_interactions))
},
#' @description Cleans out any old interactions based on the
#' re_record_interval and clean_outdated_http_interactions settings
#' @param interactions list of http interactions, of class [HTTPInteraction]
#' @return list of interactions to record
up_to_date_interactions = function(interactions) {
if (
!self$clean_outdated_http_interactions && is.null(self$re_record_interval)
) {
return(interactions)
}
Filter(function(z) {
as.POSIXct(z$recorded_at, tz = "GMT") > (as.POSIXct(Sys.time(), tz = "GMT") - self$re_record_interval)
}, interactions)
},
#' @description Should re-record interactions?
#' @return logical
should_re_record = function() {
if (is.null(self$re_record_interval)) return(FALSE)
if (is.null(self$originally_recorded_at())) return(FALSE)
now <- as.POSIXct(Sys.time(), tz = "GMT")
time_comp <- (self$originally_recorded_at() + self$re_record_interval) < now
info <- sprintf(
"previously recorded at: '%s'; now: '%s'; interval: %s seconds",
self$originally_recorded_at(), now, self$re_record_interval)
if (!time_comp) {
vcr_log_info(
sprintf("Not re-recording since the interval has not elapsed (%s).", info),
vcr_c$log_opts$date)
return(FALSE)
} else if (has_internet()) {
vcr_log_info(sprintf("re-recording (%s).", info), vcr_c$log_opts$date)
return(TRUE)
} else {
vcr_log_info(
sprintf("Not re-recording because no internet connection is available (%s).", info),
vcr_c$log_opts$date)
return(FALSE)
}
},
#' @description Is record mode NOT "all"?
#' @return logical
should_stub_requests = function() {
self$record != "all"
},
#' @description Is record mode "all"?
#' @return logical
should_remove_matching_existing_interactions = function() {
self$record == "all"
},
#' @description Get the serializer path
#' @return character
storage_key = function() self$serializer$path,
#' @description Get character string of entire cassette; bytes is a misnomer
#' @return character
raw_cassette_bytes = function() {
file <- self$file()
if (is.null(file)) return("")
tmp <- readLines(file) %||% ""
paste0(tmp, collapse = "")
},
#' @description Create the directory that holds the cassettes, if not present
#' @return no return; creates a directory recursively, if missing
make_dir = function() {
dir.create(path.expand(self$root_dir), showWarnings = FALSE,
recursive = TRUE)
},
#' @description get http interactions from the cassette via the serializer
#' @return list
deserialized_hash = function() {
tmp <- self$serializer$deserialize(self)
if (inherits(tmp, "list")) {
return(tmp)
} else {
stop(tmp, " does not appear to be a valid cassette", call. = FALSE)
}
},
#' @description get all previously recorded interactions
#' @return list
previously_recorded_interactions = function() {
if (nchar(self$raw_cassette_bytes()) > 0) {
tmp <- compact(
lapply(self$deserialized_hash()[["http_interactions"]], function(z) {
response <- VcrResponse$new(
z$response$status,
z$response$headers,
z$response$body$string %||% z$response$body$base64_string,
opts = self$cassette_opts,
disk = z$response$body$file
)
if (self$update_content_length_header)
response$update_content_length_header()
zz <- HTTPInteraction$new(
request = Request$new(z$request$method,
z$request$uri,
z$request$body$string,
z$request$headers,
disk = z$response$body$file),
response = response
)
hash <- zz$to_hash()
if (request_ignorer$should_be_ignored(hash$request)) NULL else hash
}))
return(tmp)
} else {
return(list())
}
},
#' @description write recorded interactions to disk
#' @return nothing returned
write_recorded_interactions_to_disk = function() {
if (!self$any_new_recorded_interactions()) return(NULL)
hash <- self$serializable_hash()
if (length(hash[["http_interactions"]]) == 0) return(NULL)
fun <- self$serializer$serialize()
fun(hash[[1]], self$persister$file_name, self$preserve_exact_body_bytes)
},
#' @description record an http interaction (doesn't write to disk)
#' @param x an crul or httr response object, with the request at `$request`
#' @return nothing returned
record_http_interaction = function(x) {
int <- self$make_http_interaction(x)
self$http_interactions_$response_for(int$request)
vcr_log_info(sprintf(" Recorded HTTP interaction: %s => %s",
request_summary(int$request), response_summary(int$response)),
vcr_c$log_opts$date)
self$new_recorded_interactions <- c(self$new_recorded_interactions, int)
},
#' @description Are there any new recorded interactions?
#' @return logical
any_new_recorded_interactions = function() {
length(self$new_recorded_interactions) != 0
},
#' @description make list of all options
#' @return nothing returned
make_args = function() {
self$args <- list(
record = self$record,
match_requests_on = self$match_requests_on,
re_record_interval = self$re_record_interval,
tag = self$tag, tags = self$tags,
update_content_length_header = self$update_content_length_header,
allow_playback_repeats = self$allow_playback_repeats,
allow_unused_http_interactions = self$allow_unused_http_interactions,
exclusive = self$exclusive, serialize_with = self$serialize_with,
persist_with = self$persist_with,
preserve_exact_body_bytes = self$preserve_exact_body_bytes
)
},
#' @description write metadata to the cassette
#' @return nothing returned
write_metadata = function() {
aa <- c(name = self$name, self$args)
for (i in seq_along(aa)) {
cat(sprintf("%s: %s", names(aa[i]), aa[i]),
file = sprintf("%s/%s_metadata.yml",
path.expand(cassette_path()), self$name),
sep = "\n", append = TRUE)
}
},
#' @description make [HTTPInteractionList] object, assign to http_interactions_ var
#' @return nothing returned
http_interactions = function() {
self$http_interactions_ <- HTTPInteractionList$new(
interactions = {
if (self$should_stub_requests()) {
self$previously_recorded_interactions()
} else {
list()
}
},
request_matchers = self$match_requests_on
# request_matchers = vcr_configuration()$match_requests_on
)
},
#' @description Make an `HTTPInteraction` object
#' @param x an crul or httr response object, with the request at `$request`
#' @return an object of class [HTTPInteraction]
make_http_interaction = function(x) {
# content must be raw or character
assert(unclass(x$content), c('raw', 'character'))
new_file_path <- ""
is_disk <- FALSE
if (is.character(x$content)) {
if (file.exists(x$content)) {
is_disk <- TRUE
write_disk_path <- vcr_c$write_disk_path
if (is.null(write_disk_path))
stop("if writing to disk, write_disk_path must be given; ",
"see ?vcr_configure")
new_file_path <- file.path(write_disk_path, basename(x$content))
}
}
request <- Request$new(
method = x$request$method,
uri = x$url,
body = if (inherits(x, "response")) { # httr
bd <- webmockr::pluck_body(x$request)
if (inherits(bd, "raw")) rawToChar(bd) else bd
} else { # crul
webmockr::pluck_body(x$request)
},
headers = if (inherits(x, "response")) {
as.list(x$request$headers)
} else {
x$request_headers
},
opts = self$cassette_opts,
disk = is_disk,
skip_port_stripping = TRUE
)
response <- VcrResponse$new(
status = if (inherits(x, "response")) {
c(list(status_code = x$status_code), httr::http_status(x))
} else unclass(x$status_http()),
headers = if (inherits(x, "response")) x$headers else x$response_headers,
body = if (is.raw(x$content)) {
if (can_rawToChar(x$content)) rawToChar(x$content) else x$content
} else {
stopifnot(inherits(unclass(x$content), "character"))
if (file.exists(x$content)) {
# calculate new file path in fixtures/
# copy file into fixtures/file_cache/
# don't move b/c don't want to screw up first use before using
# cached request
file.copy(x$content, write_disk_path,
overwrite = TRUE, recursive = TRUE) # copy the file
new_file_path
# raw(0)
} else {
x$content
}
},
http_version = if (inherits(x, "response")) {
x$all_headers[[1]]$version
} else {
x$response_headers$status
},
opts = self$cassette_opts,
disk = is_disk
)
if (self$update_content_length_header)
response$update_content_length_header()
HTTPInteraction$new(request = request, response = response)
},
#' @description Make a crul response object
#' @return a crul response
serialize_to_crul = function() {
if (length(self$deserialized_hash()) != 0) {
intr <- self$deserialized_hash()[[1]][[1]]
} else {
intr <- tryCatch(
self$previously_recorded_interactions()[[1]],
error = function(e) e
)
if (inherits(intr, "error")) {
intr <- tryCatch(
self$new_recorded_interactions[[1]],
error = function(e) e
)
if (inherits(intr, "error")) {
stop("no requests found to construct a crul response")
}
}
}
# request
req <- webmockr::RequestSignature$new(
method = intr$request$method,
uri = intr$request$uri,
options = list(
body = intr$request$body %||% NULL,
headers = intr$request$headers %||% NULL,
proxies = NULL,
auth = NULL
)
)
# response
resp <- webmockr::Response$new()
resp$set_url(intr$request$uri)
bod <- intr$response$body
resp$set_body(if ("string" %in% names(bod)) bod$string else bod)
resp$set_request_headers(intr$request$headers)
resp$set_response_headers(intr$response$headers)
resp$set_status(status = intr$response$status$status_code %||% 200)
# generate crul response
webmockr::build_crul_response(req, resp)
}
),
private = list(
remove_empty_cassette = function() {
if (!any(nzchar(readLines(self$file())))) {
unlink(self$file(), force = TRUE)
if (vcr_c$warn_on_empty_cassette)
warning(empty_cassette_message(self$name), call. = FALSE)
}
}
)
)
empty_cassette_message <- function(x) {
c(
sprintf("Empty cassette (%s) deleted; consider the following:\n", x),
" - If an error occurred resolve that first, then check:\n",
" - vcr only supports crul & httr; requests w/ curl, download.file, etc. are not supported\n",
" - If you are using crul/httr, are you sure you made an HTTP request?\n")
}