restfulr/0000755000175100001440000000000013141646602012141 5ustar hornikusersrestfulr/inst/0000755000175100001440000000000013140070221013101 5ustar hornikusersrestfulr/inst/unitTests/0000755000175100001440000000000013140464255015121 5ustar hornikusersrestfulr/inst/unitTests/test_RestUri.R0000644000175100001440000000241113140070221017660 0ustar hornikusers### STRATEGY: access local example Solr instance ### perform same operations as basic rsolr tests, but directly test_RestUri_construction <- function() { test.uri <- "http://example.com" rc <- RestUri(test.uri) checkIdentical(as.character(rc), sub("/$", "", test.uri)) checkIdentical(as.character(rc$sqlrest), paste0(test.uri, "/sqlrest")) checkIdentical(as.character(rc$"a space"), paste0(test.uri, URLencode("/a space"))) checkIdentical(as.character(rc[["a space"]]), paste0(test.uri, URLencode("/a space"))) checkException(RestUri(rep(test.uri, 2)), silent=TRUE) checkException(RestUri(NA), silent=TRUE) checkException(RestUri(test.uri, protocol=NULL), silent=TRUE) } test_RestUri_CRUD <- function() { solr <- rsolr::TestSolr() uri <- RestUri(solr$uri) id <- "1112211111" input <- list(id=id, name="my name!") create(uri$update$json, list(input)) response <- read(uri$update, commit="true", wt="json") checkIdentical(response$responseHeader$status, 0) response2 <- read(uri$update, commit="true", wt="json") checkIdentical(response, response2) doc <- read(uri$select, q=paste0("id:", id), wt="json") checkIdentical(doc$response$docs[[1]][1:2], input) checkException(read(uri$sqlwork)) # 404 } restfulr/inst/unitTests/test_RestContainer.R0000644000175100001440000000110613140070221021043 0ustar hornikuserstest_RestContainer_CRUD <- function() { solr <- rsolr::TestSolr() uri <- RestUri(solr$uri) id <- "1112211111" input <- list(id=id, name="my name!") container(uri$update$json)[] <- list(input) cont <- container(uri) response <- cont[["update", commit="true", wt="json"]] checkIdentical(response$responseHeader$status, 0) response2 <- cont[["update", commit="true", wt="json"]] checkIdentical(response, response2) doc <- cont[["select", q=paste0("id:", id), wt="json"]] checkIdentical(doc$response$docs[[1]][1:2], input) checkException(cont$sqlwork) } restfulr/tests/0000755000175100001440000000000013140070221013266 5ustar hornikusersrestfulr/tests/restfulr_unit_tests.R0000644000175100001440000000012213140070221017533 0ustar hornikusersrequire("restfulr") || stop("unable to load restfulr package") restfulr:::.test() restfulr/src/0000755000175100001440000000000013140464255012731 5ustar hornikusersrestfulr/src/raggedListToDF.c0000644000175100001440000000400413140464255015675 0ustar hornikusers#include SEXP R_raggedListToDF(SEXP x, SEXP uniq_nms, SEXP ind) { int *p_ind = INTEGER(ind); SEXP ans = allocVector(VECSXP, length(uniq_nms)); PROTECT(ans); setAttrib(ans, R_NamesSymbol, duplicate(uniq_nms)); for (int i = 0; i < length(x); i++) { SEXP xi = VECTOR_ELT(x, i); for (int j = 0; j < length(xi); j++, p_ind++) { int col = *p_ind - 1; SEXP xij = VECTOR_ELT(xi, j); if (xij == R_NilValue) { continue; } /* FIXME: should determine whether any length(xij) > 1 and use list */ SEXP ansj = VECTOR_ELT(ans, col); if (ansj == R_NilValue) { ansj = allocVector(TYPEOF(xij), length(x)); setAttrib(ansj, R_ClassSymbol, getAttrib(xij, R_ClassSymbol)); if (TYPEOF(ansj) == REALSXP) { for (int k = 0; k < length(ansj); k++) REAL(ansj)[k] = NA_REAL; } else if (TYPEOF(ansj) == INTSXP) { for (int k = 0; k < length(ansj); k++) INTEGER(ansj)[k] = NA_INTEGER; } else if (TYPEOF(ansj) == STRSXP) { for (int k = 0; k < length(ansj); k++) SET_STRING_ELT(ansj, k, R_NaString); } else if (TYPEOF(ansj) == LGLSXP) { for (int k = 0; k < length(ansj); k++) LOGICAL(ansj)[k] = NA_LOGICAL; } else if (TYPEOF(ansj) == VECSXP) { for (int k = 0; k < length(ansj); k++) SET_VECTOR_ELT(ansj, k, R_NilValue); } else { error("Unhandled SEXP type: %s", type2char(TYPEOF(ansj))); } SET_VECTOR_ELT(ans, col, ansj); } if (TYPEOF(ansj) == REALSXP) { REAL(ansj)[i] = asReal(xij); } else if (TYPEOF(ansj) == INTSXP) { INTEGER(ansj)[i] = asInteger(xij); } else if (TYPEOF(ansj) == STRSXP) { SET_STRING_ELT(ansj, i, asChar(xij)); } else if (TYPEOF(ansj) == LGLSXP) { LOGICAL(ansj)[i] = asLogical(xij); } else if (TYPEOF(ansj) == VECSXP) { SET_VECTOR_ELT(ansj, i, xij); } } } UNPROTECT(1); return ans; } restfulr/src/init.c0000644000175100001440000000064213140464255014042 0ustar hornikusers#include #include SEXP R_raggedListToDF(SEXP x, SEXP uniq_nms, SEXP ind); #define CALLDEF(name, n) {#name, (DL_FUNC) &name, n} static R_CallMethodDef CallEntries[] = { CALLDEF(R_raggedListToDF, 3), {NULL, NULL, 0} }; void R_init_restfulr(DllInfo *dll) { // Register C routines R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); R_useDynamicSymbols(dll, FALSE); } restfulr/NAMESPACE0000644000175100001440000000215613140073312013353 0ustar hornikusersuseDynLib(restfulr, .registration = TRUE) import(methods) importFrom(RCurl, getURLContent, base64, curlOptions, postForm, dynCurlReader, getCurlHandle, parseHTTPHeader) importFrom(utils, URLencode, URLdecode, read.csv, head, tail, write.csv) importFrom(XML, xmlInternalTreeParse, parseURI, htmlTreeParse) importFrom(rjson, fromJSON, toJSON) importFrom(S4Vectors, isSingleString, isTRUEorFALSE, recycleArg, unstrsplit) importFrom(yaml, as.yaml, yaml.load_file) importClassesFrom(S4Vectors, character_OR_NULL) export(RestUri, mediaCoercionTable, RestContainer, MediaCache, globalRestClientCache, embedCredentials) exportMethods(create, read, update, delete, purgeCache, mediaTarget, container, "container<-", credentials, username, password, authenticate) exportClasses(Media, "application/*", "audio/*", "image/*", "message/*", "model/*", "multipart/*", "text/*", "video/*", "text/plain", "text/html", "text/csv", "application/xml", "application/json", RestUri, RestContainer, CRUDProtocol, MediaCache) S3method(as.list, Media) restfulr/R/0000755000175100001440000000000013140072061012331 5ustar hornikusersrestfulr/R/MediaCache-class.R0000644000175100001440000000225313140070221015520 0ustar hornikusers### ========================================================================= ### MediaCache objects ### ------------------------------------------------------------------------- ### Just environments that only store Media objects setClass("MediaCache", contains = "environment") MediaCache <- function() { new("MediaCache", new.env(parent = emptyenv())) } setMethod("$<-", "MediaCache", function(x, name, value) { x[[name]] <- value x }) setMethod("[[<-", "MediaCache", function(x, i, j, ..., value) { if (!missing(j)) warning("argument 'j' is ignored") assign(i, value, x) x }) setGeneric("assign", function (x, value, pos = -1, envir = as.environment(pos), inherits = FALSE, immediate = TRUE) standardGeneric("assign"), signature = c("x", "value")) setMethod("assign", "MediaCache", function (x, value, pos = -1, envir = as.environment(pos), inherits = FALSE, immediate = TRUE) { if (!is(value, "Media")) stop("MediaCache only permits Media storage") callNextMethod() }) purge <- function(x) { rm(list=ls(x), envir=x) x } restfulr/R/utils.R0000644000175100001440000000125513140070221013613 0ustar hornikusers### ========================================================================= ### Low-level utilities ### ------------------------------------------------------------------------- raggedListToDF <- function(x, keepAlwaysNULL = TRUE, ...) { nms <- unlist(lapply(x, names)) uniq.nms <- unique(nms) ind <- match(nms, uniq.nms) cols <- .Call(R_raggedListToDF, x, uniq.nms, ind) nulls <- vapply(cols, is.null, logical(1L)) if (keepAlwaysNULL) { cols[nulls] <- list(rep(NA, length(x))) } else { cols[nulls] <- NULL } as.data.frame(cols, ...) } setMethod("unstrsplit", "AsIs", function(x, sep = "") { unstrsplit(unclass(x), sep=sep) }) restfulr/R/Credentials-class.R0000644000175100001440000000335213140070221016013 0ustar hornikusers### ========================================================================= ### Credentials objects ### ------------------------------------------------------------------------- setClass("Credentials", representation(username = "character_OR_NULL", password = "character_OR_NULL"), validity = function(object) { c(if (!is.null(object@username) && !isSingleString(object@username)) "username must be NULL or a single string", if (!is.null(object@password) && !isSingleString(object@password)) "password must be NULL or a single string") }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Accessors ### setGeneric("username", function(x) standardGeneric("username")) setMethod("username", "Credentials", function(x) x@username) setGeneric("password", function(x) standardGeneric("password")) setMethod("password", "Credentials", function(x) x@password) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Constructor ### Credentials <- function(username, password) { new("Credentials", username=username, password=password) } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Coerce ### as.list.Credentials <- function(x) { as.list(x) } setMethod("as.list", "Credentials", function(x) list(username=username(x), password=password(x))) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Show ### setMethod("show", "Credentials", function(object) { cat("A", class(object), "object\n") cat("username:", username(object), "\n") }) restfulr/R/CRUDProtocol-class.R0000644000175100001440000000172613140070221016040 0ustar hornikusers### ========================================================================= ### CRUDProtocol objects ### ------------------------------------------------------------------------- ### Implements a specific REST protocol (like HTTP). .CRUDProtocol <- setRefClass("CRUDProtocol") .CRUDProtocol$methods(create = function(x, ..., value) { unimplemented("create") }) unimplemented <- function(x) { stop("This protocol does not implement the '", x, "' operation") } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Constructor ### ### FIXME: may be incomplete availableCRUDProtocols <- function() { names(getClass("CRUDProtocol")@subclasses) } CRUDProtocol <- function(uri, ...) { if (!isSingleString(uri)) { stop("'uri' must be a single, non-NA string") } protocol <- toupper(parseURI(uri)$scheme) if (!protocol %in% availableCRUDProtocols()) { stop("Unsupported CRUD protocol: ", protocol) } match.fun(protocol)(...) } restfulr/R/test_restfulr_package.R0000644000175100001440000000043613140070221017033 0ustar hornikusers.test <- function() { if (!requireNamespace("rsolr")) { warning("install rsolr to perform tests") return() } solr <- rsolr::TestSolr() on.exit(solr$kill()) testPackage <- get("testPackage", getNamespace("BiocGenerics")) testPackage("restfulr") } restfulr/R/Media-class.R0000644000175100001440000001153113140070221014573 0ustar hornikusers### ========================================================================= ### Formal declaration of media types ### ------------------------------------------------------------------------- setClass("Media", representation(cacheInfo = "CacheInfo")) setClass("application/*", contains = "Media") setClass("audio/*", contains = "Media") setClass("image/*", contains = "Media") setClass("message/*", contains = "Media") setClass("model/*", contains = "Media") setClass("multipart/*", representation(boundary = "character"), contains = "Media") setClass("text/*", representation(charset = "character"), prototype(charset = "us-ascii"), contains = c("character", "Media")) setClass("video/*", contains = "Media") setClass("text/plain", contains = "text/*") setClass("text/html", contains = "text/*") setClass("text/csv", contains = "text/*") setClass("application/xml", representation(charset = "character"), prototype(charset = "us-ascii"), contains = c("character", "application/*")) setClass("application/xhtml+xml", contains="application/xml") setClass("application/json", contains = c("character", "application/*")) ##setClass("application/R", contains = "application/*") setClass("application/x-www-form-urlencoded", contains = c("character", "application/*")) setClass("NullMedia", contains = "Media") ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Accessors ### setGeneric("cacheInfo", function(x, ...) standardGeneric("cacheInfo")) setMethod("cacheInfo", "Media", function(x) x@cacheInfo) setMethod("cacheInfo", "NULL", function(x) NULL) setGeneric("cacheInfo<-", function(x, ..., value) standardGeneric("cacheInfo<-")) setReplaceMethod("cacheInfo", c("Media", "CacheInfo"), function(x, value) { x@cacheInfo <- value x }) setMethod("expired", "Media", function(x) expired(cacheInfo(x))) setMethod("length", "NullMedia", function(x) 0L) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Coercion ### ## Default R <-> media type mapping setGeneric("mediaTarget", function(x, ...) standardGeneric("mediaTarget")) setMethod("mediaTarget", "application/json", function(x) "list") setMethod("mediaTarget", "application/xml", function(x) "XMLAbstractNode") setMethod("mediaTarget", "text/html", function(x) "XMLAbstractNode") setMethod("mediaTarget", "text/csv", function(x) "data.frame") setMethod("mediaTarget", "text/*", function(x) "character") setMethod("mediaTarget", "NullMedia", function(x) "NULL") setAs("application/xml", "XMLAbstractNode", function(from) { xmlInternalTreeParse(from, asText=TRUE) }) setAs("text/html", "XMLAbstractNode", function(from) { htmlTreeParse(from, asText=TRUE, useInternalNodes=TRUE) }) setAs("application/json", "list", function(from) { fromJSON(from) }) `as.data.frame.application/json` <- function(x, row.names = NULL, optional = FALSE, ...) { df <- raggedListToDF(as.list(x), optional=optional, ...) if (!is.null(row.names)) rownames(df) <- row.names df } `as.data.frame.text/csv` <- function(x, row.names = NULL, optional = FALSE, ...) { chr <- as.character(x) if (identical(chr, "") || identical(chr, "\n")) { return(data.frame()) } con <- file() on.exit(close(con)) writeLines(chr, con) ### FIXME: we are assuming a header, but there is no guarantee df <- read.csv(con, check.names=!optional, stringsAsFactors=FALSE, ...) if (!is.null(row.names)) rownames(df) <- row.names df } setAs("Media", "data.frame", function(from) { as.data.frame(from, optional=TRUE) }) setAs("ANY", "Media", function(from) { as(from, "application/json") }) setAs("list", "application/json", function(from) { new("application/json", toJSON(from)) }) setAs("data.frame", "Media", function(from) { as(from, "text/csv") }) setAs("data.frame", "text/csv", function(from) { con <- file() on.exit(close(con)) df <- as(from, "data.frame") df[] <- lapply(df, function(x) { if (is.list(x)) { unstrsplit(x, ",") } else { x } }) write.csv(df, con, row.names=FALSE) new("text/csv", paste(readLines(con), collapse="\n")) }) as.list.Media <- function(x, ...) as(x, "list") contentType <- function(x) { slots <- setdiff(slotNames(class(x)), c(".Data", "cacheInfo")) params <- vapply(slots, function(nm) as.character(slot(x, nm)), character(1L)) paste(c(class(x), paste(names(params), params, sep="=")), collapse=";") } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Show ### setMethod("show", "Media", function(object) { cat("Media of type '", class(object), "'\n", sep = "") cat("length: ", length(object), "\n", sep = "") }) restfulr/R/CacheInfo-class.R0000644000175100001440000000200513140070221015367 0ustar hornikusers### ========================================================================= ### CacheInfo class ### ------------------------------------------------------------------------- setClass("CacheInfo", representation(expires = "POSIXt", lastModified = "POSIXt", hash = "character_OR_NULL")) CacheInfo <- function(expires = Sys.time(), lastModified = as.POSIXct("1960-01-01"), hash = NULL) { if (!is.null(hash) && !isSingleString(hash)) stop("If not NULL, 'hash' must be a single, non-NA string") new("CacheInfo", expires = expires, lastModified = lastModified, hash = hash) } setGeneric("expired", function(x, ...) standardGeneric("expired")) setMethod("expired", "CacheInfo", function(x) { x@expires < Sys.time() }) setGeneric("modifiedSince", function(x, date, ...) standardGeneric("modifiedSince")) setMethod("modifiedSince", c("CacheInfo", "Date"), function(x, date) { x@lastModified >= date }) restfulr/R/RestContainer-class.R0000644000175100001440000000521313140070221016334 0ustar hornikusers### ========================================================================= ### RestContainer objects ### ------------------------------------------------------------------------- ### ### A means for accessing a URI using vector-like syntax. ### setClass("RestContainer", representation(uri="RestUri")) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Constructor ### RestContainer <- function(...) { container(RestUri(...)) } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### CREATE ### ## How about: service$objects[] <- new.objects. ## The syntax service$objects[] means extract everything, because the ## default index is the wildcard. Intuitively, one would expect the ## same for []<-, i.e., replace-all; however, we could take the ## default to be the IDs of the elements being added. This is a ## primary difference from a DB API and R vectors: with databases, the ## IDs tend to be inherent in the objects. setMethod("[<-", "RestContainer", function(x, i, j, ..., value) { if (!missing(j)) warning("argument 'j' is ignored") if (missing(i)) { create(x@uri, value, ...) } else { value <- recycleArg(value, "value", length(i)) for (ii in i) x[[ii, ...]] <- value[[ii]] } x }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### READ ### setMethod("$", "RestContainer", function(x, name) { x[[name]] }) setMethod("[[", "RestContainer", function(x, i, ...) { if (!is.character(i)) stop("'i' must be a character vector") read(x@uri[[i]], ...) }) setMethod("[", "RestContainer", function(x, i, j, ..., drop = TRUE) { if (!missing(j)) warning("argument 'j' is ignored") if (!isTRUE(drop)) warning("argument 'drop' must be TRUE") if (missing(i)) { read(x, ...) } else { if (!is.character(i)) stop("'i' must be a character vector") sapply(i, function(ii, ...) x[[ii, ...]], ..., simplify=FALSE) } }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### UPDATE/DELETE ### setMethod("$<-", "RestContainer", function(x, name, value) { x[[name]] <- value x }) setMethod("[[<-", "RestContainer", function(x, i, j, ..., value) { if (missing(i)) stop("argument 'i' cannot be missing") if (!missing(j)) warning("argument 'j' is ignored") if (is.null(value)) delete(x@uri[[i]], ...) else update(x@uri[[i]], ..., value=value) x }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Show ### setMethod("show", "RestContainer", function(object) { cat("RestContainer object\n") cat("uri:", as.character(object@uri), "\n") }) restfulr/R/RestUri-class.R0000644000175100001440000002226313140073275015171 0ustar hornikusers### ========================================================================= ### RestUri objects ### ------------------------------------------------------------------------- ## ## A URI constructor with a $/[[-based syntax for path concatenation. ## ## Could be called more specifically a URL, since it can get the data. ## setClassUnion("CredentialsORNULL", c("Credentials", "NULL")) setClass("RestUri", representation(cache = "MediaCache", protocol = "CRUDProtocol", credentials = "CredentialsORNULL"), contains = "character") ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Constructor ### globalRestClientCache <- local({ cache <- MediaCache() function() cache }) RestUri <- function(base.uri, protocol = CRUDProtocol(base.uri, ...), cache = globalRestClientCache(), ...) { if (!isSingleString(base.uri)) stop("'base.uri' must be a single, non-NA string") if (!missing(protocol) && length(list(...)) > 0L) warning("arguments in '...' are ignored when 'protocol' is non-missing") base.uri <- sub("/$", "", base.uri) credentials <- findCredentials(base.uri) new("RestUri", base.uri, protocol = protocol, cache = cache, credentials = credentials) } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Accessors ### setGeneric("credentials", function(x, ...) standardGeneric("credentials")) setMethod("credentials", "RestUri", function(x) x@credentials) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### URI building methods ### ### Two choices for syntax: ### 1) Constructive: c(uri, "foo", "bar") ### 2) Accessive: uri$foo$bar ### The latter is preferable -- we are accessing a resource / api setMethod("$", "RestUri", function(x, name) { x[[name]] }) setMethod("[[", "RestUri", function(x, i, j, ...) { if (!missing(j) || length(list(...)) > 0L) warning("argument 'j' and arguments in '...' are ignored") tokens <- as.character(lapply(i, URLencode)) initialize(x, paste0(x, "/", paste(tokens, collapse="/"))) }) setReplaceMethod("$", "RestUri", function(x, name, value) { x }) setReplaceMethod("[[", "RestUri", function(x, i, j, ..., value) { x }) setMethod("[", "RestUri", function(x, i, j, ...) { params <- c(i=if (!missing(i)) i, j=if (!missing(j)) j, list(...)) query(x, params) }) query <- function(x, ...) { query.params <- c(...) query <- paste(sapply(names(query.params), URLencode, reserved=TRUE), sapply(as.character(query.params), URLencode, reserved=TRUE), sep = "=", collapse = "&") if (nchar(query) > 0L) { uri <- if (grepl("?", x, fixed=TRUE)) paste0(x, "&", query) else paste0(x, "?", query) initialize(x, uri) } else { x } } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### RestContainer factory ### setGeneric("container", function(x, ...) standardGeneric("container")) setMethod("container", "RestUri", function(x) { new("RestContainer", uri=x) }) setGeneric("container<-", function(x, ..., value) standardGeneric("container<-")) setReplaceMethod("container", "RestUri", function(x, value) { x }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Transactions ### write <- function(FUN, url, value, ..., returnResponse=FALSE) { stopifnot(isTRUEorFALSE(returnResponse)) url <- query(url, ...) media <- as(value, "Media", strict=FALSE) media <- tryCatch(FUN(url, media), unauthorized = function(cond) { url <- authenticate(url) FUN(url, media) }) if (returnResponse) as(media, mediaTarget(media)) else invisible(url) } setGeneric("create", function(x, ...) standardGeneric("create")) setMethod("create", "RestUri", function(x, value, ..., returnResponse=FALSE) { write(x@protocol$create, x, value, ..., returnResponse=returnResponse) }) setMethod("create", "character", function(x, ...) { create(RestUri(x), ...) }) setGeneric("read", function(x, ...) standardGeneric("read")) setMethod("read", "RestUri", function(x, ...) { x <- query(x, ...) cached.media <- x@cache[[x]] if (!is.null(cached.media) && !expired(cached.media)) media <- cached.media else { if(isTRUE(getOption("verbose"))) { message("READ: ", URLdecode(x)) } cacheInfo <- cacheInfo(cached.media) result <- tryCatch(x@protocol$read(x, cacheInfo), unauthorized = function(cond) { x <- authenticate(x) x@protocol$read(x, cacheInfo) }) if (is(result, "CacheInfo")) { cacheInfo(cached.media) <- result media <- cached.media } else { media <- result } x@cache[[x]] <- media } as(media, mediaTarget(media)) }) setMethod("read", "character", function(x, ...) { read(RestUri(x), ...) }) setMethod("update", "RestUri", function(object, value, ...) { write(object@protocol$update, object, value, ...) }) setMethod("update", "character", function(object, value, ...) { update(RestUri(object), value, ...) }) setGeneric("delete", function(x, ...) standardGeneric("delete")) setMethod("delete", "RestUri", function(x, ...) { x <- query(x, ...) invisible(tryCatch(x@protocol$delete(x), unauthorized = function(cond) { x <- authenticate(x) x@protocol$delete(x) })) }) setMethod("delete", "character", function(x, ...) { delete(RestUri(x), ...) }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Authentication ### configPath <- function(...) { base <- path.expand(file.path(Sys.getenv("XDG_CONFIG_HOME", "~/.local/config"), "restfulr")) file.path(base, ...) } loadCredentials <- function() { path <- configPath("credentials.yaml") if (file.exists(path)) { yaml.load_file(path) } else { list() } } findCredentials <- function(uri) { config <- loadCredentials() hits <- which(startsWith(uri, as.character(names(config)))) if (length(hits) > 0L) { userpwd <- config[[hits[1L]]] Credentials(userpwd$username, userpwd$password) } } saveCredentials <- function(x, uri) { config <- loadCredentials() config[[uri]] <- as.list(x) path <- configPath("credentials.yaml") if (!file.exists(dirname(path))) { dir.create(dirname(path), recursive=TRUE) } writeLines(as.yaml(config), path) } hackyGetPass <- function() { inTerminal <- .Platform$GUI == "X11" && !identical(Sys.getenv("EMACS"), "t") if (inTerminal) { system("stty -echo") on.exit(system("stty echo")) } else { cat("THIS WILL SHOW YOUR PASSWORD\n") } readline("password: ") } promptForCredentials <- function() { defaultUser <- Sys.info()["effective_user"] username <- readline(paste0("username (default: ", defaultUser, "): ")) if (username == "") username <- defaultUser if (requireNamespace("getPass")) { password <- getPass::getPass() } else { password <- hackyGetPass() } if (!is.null(password) && password != "") Credentials(username, password) } unauthorized <- function() { error <- simpleError("unauthorized") class(error) <- c("unauthorized", class(error)) stop(error) } setGeneric("authenticate", function(x, ...) standardGeneric("authenticate")) setMethod("authenticate", "RestUri", function(x) { credentials <- findCredentials(x) if (is.null(credentials)) { credentials <- promptForCredentials() if (is.null(credentials)) { stop("authentication failed: see ?credentials") } if (getOption("restfulr.store.credentials", TRUE)) { saveCredentials(credentials, x) } } initialize(x, credentials=credentials) }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Utilities ### setGeneric("purgeCache", function(x, ...) standardGeneric("purgeCache")) setMethod("purgeCache", "RestUri", function(x) { purge(x@cache) x }) embedCredentials <- function(x) { creds <- credentials(x) if (is.null(creds)) { return(x) } auth <- paste0(URLencode(username(creds), reserved=TRUE), ":", URLencode(password(creds), reserved=TRUE), "@") url <- sub("://", paste0("://", auth), x, fixed=TRUE) initialize(x, url, credentials=NULL) } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Show ### setMethod("show", "RestUri", function(object) { cat("RestUri:", as.character(object), "\n") }) restfulr/R/HTTP-class.R0000644000175100001440000001455413140070221014343 0ustar hornikusers### ========================================================================= ### HTTP protocol implementation ### ------------------------------------------------------------------------- .HTTP <- setRefClass("HTTP", fields = list( accept = "character" ), contains = "CRUDProtocol") ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Constructor ### HTTP <- function(accept = acceptedMediaTypes()) { if (!is.character(accept) || any(is.na(accept))) stop("'accept' must be a character() without NAs") .HTTP$new(accept = accept) } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### CRUD implementation ### .HTTP$methods(create = function(x, value, ...) { if (!isSingleString(x)) stop("'x' must be a single, non-NA string representing a URL") curl <- getCurlHandle() reader <- dynCurlReader(curl) opts <- curlOptions(postfields = paste(value, collapse="\n"), httpheader = c( Accept = accept(.self), 'Content-Type' = contentType(value), Authorization = authorization(x), ...), headerfunction = reader$update) content <- try(postForm(x, .opts=opts, curl=curl), silent=TRUE) invisible(handleResponse(content, reader)) }) .HTTP$methods(read = function(x, cache.info, ...) { if (!isSingleString(x)) stop("'x' must be a single, non-NA string representing a URL") request.header <- c(headerFromCacheInfo(cache.info), Authorization = authorization(x), Accept = accept(.self), ...) ## We use our own reader so that we can return the body in case of error curl <- getCurlHandle(httpheader = request.header) reader <- dynCurlReader(curl) content <- try(getURLContent(x, header = reader, curl = curl), silent=TRUE) handleResponse(content, reader, cache.info) }) .HTTP$methods(update = function(x, ..., value) { stop("PUT support not yet implemented") }) .HTTP$methods(delete = function(x, ...) { stop("DELETE support not yet implemented") }) ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Helpers ### handleResponse <- function(content, reader, cache.info = NULL) { response <- list(header = parseHTTPHeader(reader$header()), body = reader$value()) status <- as.integer(response$header["status"]) if (identical(status, HTTP_STATUS$Unauthorized)) { unauthorized() } if (is(content, "try-error")) { stop(structure(attr(content, "condition"), body=responseToMedia(response))) } if (identical(status, HTTP_STATUS$No_Content)) { response <- NULL } if (!is.null(cache.info) && identical(status, HTTP_STATUS$Not_Modified)) { cacheInfoFromHeader(response$header, cache.info) } else { responseToMedia(response) } } coercionTable <- function() { signatures <- names(getMethods(coerce, table = TRUE)) matrix(unlist(strsplit(signatures, "#")), ncol=2L, byrow=TRUE, dimnames=list(NULL, c("from", "to"))) } mediaCoercionTable <- function() { tab <- coercionTable() classes <- names(getClass("Media")@subclasses) tab[rowSums(matrix(tab %in% classes, ncol=2L)) == 1L,] } acceptedMediaTypes <- function() { intersect(mediaCoercionTable()[,"from"], names(getClass("Media")@subclasses)) } responseToMedia <- function(x) { content.type <- head(attr(x$body, "Content-Type"), 1) content.params <- tail(attr(x$body, "Content-Type"), -1) media.class <- mediaClassFromContentType(content.type) content.params <- content.params[intersect(names(content.params), slotNames(media.class))] if (media.class == "NullMedia") { new("NullMedia", cacheInfo = cacheInfoFromHeader(x$header)) } else { do.call(new, c(media.class, x$body, cacheInfo = cacheInfoFromHeader(x$header), content.params)) } } mediaClassFromContentType <- function(x) { if (is.null(x)) "NullMedia" else if (is.character(x)) { if (isClass(x)) x else sub("/.*", "/*", x) } else stop("content type should be character or NULL") } headerFromCacheInfo <- function(x) { if (is.null(x)) NULL else c("If-None-Match" = x@hash, "If-Modified-Since" = formatHTTPDate(x@lastModified)) } cacheInfoFromHeader <- function(x, original = CacheInfo()) { x <- as.list(x) cache.control <- parseCacheControl(x[["Cache-Control"]]) if (isTRUE(cache.control[["no-cache"]])) expires <- Sys.time() else if (!is.null(cache.control[["max-age"]])) expires <- Sys.time() + cache.control[["max-age"]] else expires <- parseHTTPDate(x[["Expires"]]) info.args <- list(expires = expires, lastModified = parseHTTPDate(x[["Last-Modified"]]), hash = x[["ETag"]]) info.args <- Filter(Negate(is.null), info.args) do.call(initialize, c(original, info.args)) } parseCacheControl <- function(x) { if (is.null(x)) return(NULL) fields <- strsplit(x, ", ")[[1]] key.val <- strsplit(fields, "=") keys <- sapply(key.val, head, 1) has.val <- sapply(key.val, length) > 1L l <- list() l[keys[!has.val]] <- TRUE l[keys[has.val]] <- sapply(key.val[has.val], tail, 1) if (!is.null(l[["max-age"]])) l[["max-age"]] <- as.integer(l[["max-age"]]) l } .httpParseDateString <- "%a, %d %b %Y %H:%M:%S" .httpFormatDateString <- paste(.httpParseDateString, "%Z") formatHTTPDate <- function(x) { format(x, .httpFormatDateString, tz = "GMT") } parseHTTPDate <- function(x) { if (is.null(x)) NULL else strptime(x, .httpParseDateString, tz = "GMT") } authorization <- function(x) { credentials <- credentials(x) if (!is.null(credentials)) { auth <- paste0(username(credentials), ":", password(credentials)) paste("Basic", base64(auth)) } } accept <- function(x) { paste(x$accept, collapse=", ") } stopIfHTTPError <- function(header) { stop.if.HTTP.error <- get("stop.if.HTTP.error", getNamespace("RCurl")) stop.if.HTTP.error(header) } ### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ### Status code constants ### HTTP_STATUS <- setNames(as.list(as.integer(names(RCurl:::httpErrorClasses))), RCurl:::httpErrorClasses) restfulr/MD50000644000175100001440000000236313141646602012455 0ustar hornikuserscf32a2d70a0e286a4ea862aad9578a0c *DESCRIPTION c3f3f72bc3f7a3fb30bd736e66720065 *NAMESPACE 2155d32161de95a5f6052345fe3479de *R/CRUDProtocol-class.R fb10bcf8423a6800535c2332acb2ec4c *R/CacheInfo-class.R 16131e3379c630cc6fbb7bd450c5946d *R/Credentials-class.R 0af7dcd961afff7cd1d722215139883d *R/HTTP-class.R 8217e42348252bdc3b5d62c65e20f7e6 *R/Media-class.R 7dc8e6e9e335004a5cf7381f26d93b7c *R/MediaCache-class.R 5861200ecec680fec9dd9b2fa168ca7e *R/RestContainer-class.R fd2dba913b3add9e4ba66b830e53661d *R/RestUri-class.R 0fcd40919deee51e1c04eaffb6ef06f1 *R/test_restfulr_package.R d5dd93d1257549a6f361b4a50d5c73a8 *R/utils.R 9e409d49b5ea5e4b53eb6e1ae620d90f *inst/unitTests/test_RestContainer.R 6724bc7a7df9001e8a8ba0dc1b513174 *inst/unitTests/test_RestUri.R 6594f85f9f2dc08bbf321f258a93435f *man/CRUDProtocol-class.Rd 11b4f6f1b6cd32e4fd234a3a6f3af2a2 *man/Credentials-class.Rd 157ca009e829d8f39e4b3663d01ae141 *man/Media-class.Rd 23dba3adf5431636eeb3d71e200e40c6 *man/MediaCache-class.Rd b2287d97c7da3c26edc2146fe58b42c2 *man/RestContainer-class.Rd b4f5f554748eaac3b9733696ee43c12e *man/RestUri-class.Rd 4a71c2f1009d487f95fcaa1ced1c7af3 *src/init.c 04f3c27c1d49256ee7812f593a04b1e1 *src/raggedListToDF.c 7aea4fb9179fb3637c4df6e5a689c2b3 *tests/restfulr_unit_tests.R restfulr/DESCRIPTION0000644000175100001440000000127313141646602013652 0ustar hornikusersPackage: restfulr Type: Package Title: R Interface to RESTful Web Services Version: 0.0.13 Author: Michael Lawrence Maintainer: Michael Lawrence Description: Models a RESTful service as if it were a nested R list. License: Artistic-2.0 Imports: XML, RCurl, rjson, S4Vectors (>= 0.13.15), yaml Depends: R (>= 3.4.0), methods Suggests: getPass, rsolr, RUnit Collate: CRUDProtocol-class.R CacheInfo-class.R Credentials-class.R HTTP-class.R Media-class.R MediaCache-class.R RestUri-class.R RestContainer-class.R test_restfulr_package.R utils.R NeedsCompilation: yes Packaged: 2017-08-03 00:05:01 UTC; larman Repository: CRAN Date/Publication: 2017-08-06 17:10:26 UTC restfulr/man/0000755000175100001440000000000013140073477012717 5ustar hornikusersrestfulr/man/Media-class.Rd0000644000175100001440000000644113140070221015315 0ustar hornikusers\name{Media-class} \docType{class} \alias{class:Media} \alias{Media-class} \alias{class:application/*} \alias{application/*-class} \alias{class:audio/*} \alias{audio/*-class} \alias{class:image/*} \alias{image/*-class} \alias{class:message/*} \alias{message/*-class} \alias{class:model/*} \alias{model/*-class} \alias{class:multipart/*} \alias{multipart/*-class} \alias{class:text/*} \alias{text/*-class} \alias{class:video/*} \alias{video/*-class} \alias{class:text/plain} \alias{text/plain-class} \alias{class:text/html} \alias{text/html-class} \alias{class:text/csv} \alias{text/csv-class} \alias{class:application/xml} \alias{application/xml-class} \alias{class:application/json} \alias{application/json-class} % Utilities: \alias{mediaCoercionTable} \alias{length,NullMedia-method} % Coercion: \alias{mediaTarget} \alias{mediaTarget,application/json-method} \alias{mediaTarget,application/xml-method} \alias{mediaTarget,text/html-method} \alias{mediaTarget,text/csv-method} \alias{mediaTarget,text/*-method} \alias{mediaTarget,NullMedia-method} \alias{coerce,application/json,list-method} \alias{coerce,data.frame,text/csv-method} \alias{coerce,application/xml,XMLAbstractNode-method} \alias{coerce,text/csv,data.frame-method} \alias{coerce,ANY,Media-method} \alias{coerce,list,application/json-method} \alias{coerce,data.frame,Media-method} % Show: \alias{show,Media-method} \title{Media} \description{ The \code{Media} object represents a value identified by a URI. There is a \code{Media} subclass for each media type, such as \dQuote{text/csv} or \dQuote{application/xml}. Coercion methods (see \code{\link{setAs}}) define mappings between \code{Media} subclasses and \R objects. The user does not usually need to access this functionality directly. } \section{Type conversion}{ Each \code{Media} subclass may be converted to/from one or more \R types. The mappings are established by \code{\link{setAs}}. The following bi-directional mappings are built into the package: \tabular{ll}{ \code{application/xml}, \code{text/html} \tab \code{\link[XML:XMLAbstractNode-class]{XMLAbstractNode}} \cr \code{application/json} \tab \code{list} \cr \code{text/csv} \tab \code{data.frame} \cr \code{text/*} \tab \code{character} } But call \code{mediaCoercionTable} to see what is defined in the current session. The \code{\link{as}} function is the canonical interface to converting media to \R objects. It (usefully) requires that the user specify the target \R type. For convenience, the \code{mediaTarget} generic returns the default R type for a given \code{Media} object. To support a new media type, one should define a \code{Media} subclass with the same name as the media type (\code{application/xml}), a corresponding \code{mediaTarget} method, and all relevant \code{coerce} methods. See the \code{Media} class hierarchy to determine where the new type fits. } \section{Helpers}{ \describe{ \item{}{\code{mediaCoercionTable()}: Returns a character matrix with columns \dQuote{from} and \dQuote{to}, indicating the available coercions of media types to/from \R objects. } } } \examples{ txt <- '{"json":{"rocks":true}}' json <- as(txt, "application/json") as(json, mediaTarget(json)) } \author{ Michael Lawrence } \keyword{methods} \keyword{classes} restfulr/man/RestUri-class.Rd0000644000175100001440000001170713140073477015714 0ustar hornikusers\name{RestUri-class} \docType{class} \alias{class:RestUri} \alias{RestUri-class} % CRUD: \alias{create} \alias{create,RestUri-method} \alias{read} \alias{read,RestUri-method} \alias{update} \alias{update,RestUri-method} \alias{delete} \alias{delete,RestUri-method} \alias{create,character-method} \alias{read,character-method} \alias{update,character-method} \alias{delete,character-method} % Construction: \alias{RestUri} \alias{$,RestUri-method} \alias{[[,RestUri-method} \alias{$<-,RestUri-method} \alias{[[<-,RestUri-method} \alias{[,RestUri-method} % Container: \alias{container} \alias{container,RestUri-method} \alias{container<-} \alias{container<-,RestUri-method} % Utilities \alias{purgeCache} \alias{purgeCache,RestUri-method} \alias{embedCredentials} % Authentication: \alias{authenticate} \alias{authenticate,RestUri-method} \alias{credentials,RestUri-method} % Show: \alias{show,RestUri-method} \title{RestUri} \description{ The \code{RestUri} object represents a resource accessible via a RESTful interface. It extends \code{character} with a protocol, used for accessing the data, as well as a cache. R objects are converted to/from external media via the \code{\linkS4class{Media}} framework. } \section{CRUD interface}{ There are four canonical, abstract types of REST operations: create, read, update, delete (CRUD). The CRUD model was borrowed from traditional databases. The restfulr package maps those four operations to R functions of the same name. The functions are generic, and there are methods for \code{RestUri} and \code{character} (taken to be a URI), described below. \describe{ \item{}{ \code{create(x, value, ..., returnResponse=FALSE)}: Creates a resource at \code{x} by converting \code{value} to a supported media type. The \dots become query parameters on \code{x}. If \code{returnResponse} is \code{TRUE}, convert and return any response sent from the endpoint. By default, \code{x} is returned, to support chaining. This corresponds to an HTTP \code{POST}. } \item{}{ \code{read(x, ...)}: Reads the resource at \code{x}, coerces it to an R object, and returns the object. The \dots become query parameters on \code{x}. This corresponds to an HTTP \code{GET}. } \item{}{ \code{update(object, value, ...)}: Updates the resource at \code{x} by converting \code{value} to a supported media type. The \dots become query parameters on \code{x}. This corresponds to an HTTP \code{PUT}. } \item{}{ \code{delete(x, ...)}: Deletes the resource at \code{x}. This corresponds to an HTTP \code{DELETE}. } } } \section{Constructor}{ \describe{ \item{}{ \code{RestUri(base.uri, protocol = CRUDProtocol(base.uri, ...), cache = globalRestClientCache(), ...)}: Constructs a \code{RestUri} object, pointing to \code{base.uri}, a string representation of the URI. The \code{protocol} (a \code{\linkS4class{CRUDProtocol}} instance) is automatically determined from the scheme of the URI. By default, all instances share the same global \code{cache}, a \code{\linkS4class{MediaCache}} instance. } \item{}{\code{x$name}: Extends the path of \code{x} by appending \code{name}. This is a convenient way to narrow a URI and is intuitive if one thinks of a tree of resources as a nested list. } \item{}{\code{x[[i]]}: Extends the path of \code{x} by appending \code{i}. } \item{}{\code{x[...]}: Named arguments in \code{...} become query parameters on \code{x}. } } } \section{Container support}{ \describe{ \item{}{ \code{container(x)}: Gets a \code{\linkS4class{RestContainer}} object for treating \code{x} as a list-like container. } } } \section{Authentication}{ RestUri currently supports basic HTTP authentication. Call \code{authenticate(x)} to add credentials to the RestUri \code{x}. Retrieve the Credentials object with the \code{credentials} accessor. Once a set of credentials has been entered, it is recorded for the URI in \file{$(HOME)/.local/config/restfulr/credentials.yaml}. The path prefix can be changed via the \env{XDG_CONFIG_DIR} environment variable, according to the \acronym{XDG} standard. The credential cache is checked during authentication, so that a user does not need to reenter credentials for the same URI. If the \pkg{getPass} package is installed, we use it for password entry. Otherwise, we rely on an implementation that shows the password as it is entered, unless the user is in a terminal, where we can hide input. } \section{Utilities}{ \describe{ \item{}{\code{embedCredentials(x)}: Embeds the internal credential information into the URL itself, for interfacing with other tools, like \code{utils::download.file}. } } } \examples{ apache <- RestUri("http://wiki.apache.org") read(apache$solr) } \author{ Michael Lawrence } \keyword{methods} \keyword{classes} restfulr/man/Credentials-class.Rd0000644000175100001440000000131213140070221016523 0ustar hornikusers\name{Credentials-class} \docType{class} \alias{class:Credentials} \alias{Credentials-class} \alias{credentials} \alias{username} \alias{username,Credentials-method} \alias{password} \alias{password,Credentials-method} \title{Credentials} \description{ \code{Credentials} stores authentication credentials (username and password). Note that it is easy to retrieve the password from this object. There is no encryption or other security. } \section{Accessors}{ \describe{ \item{}{ \code{username}: get the username as a string. } \item{}{ \code{password}: get the password as a plain text string. } } } \author{ Michael Lawrence } \keyword{methods} \keyword{classes} restfulr/man/MediaCache-class.Rd0000644000175100001440000000106413140070221016235 0ustar hornikusers\name{MediaCache-class} \docType{class} \alias{class:MediaCache} \alias{MediaCache-class} \alias{$<-,MediaCache-method} \alias{[[<-,MediaCache-method} \alias{MediaCache} \alias{globalRestClientCache} \title{MediaCache} \description{ \code{MediaCache} is just an environment that restricts the types of its elements to \code{\linkS4class{Media}}. Construct an instance by calling \code{MediaCache}. The shared default for all REST clients is returned by \code{globalRestClientCache}. } \author{ Michael Lawrence } \keyword{methods} \keyword{classes} restfulr/man/CRUDProtocol-class.Rd0000644000175100001440000000101513140070221016545 0ustar hornikusers\name{CRUDProtocol-class} \docType{class} \alias{class:CRUDProtocol} \alias{CRUDProtocol-class} \title{CRUDProtocol} \description{ \code{CRUDProtocol} is a base class for implementing the communication protocol for performing Create, Read, Update and Delete (CRUD) operations on a resource identified by a \code{\linkS4class{RestUri}}. Only HTTP is implemented by this package, and it should serve as useful example for other implementations. } \author{ Michael Lawrence } \keyword{methods} \keyword{classes} restfulr/man/RestContainer-class.Rd0000644000175100001440000000515513140070221017057 0ustar hornikusers\name{RestContainer-class} \docType{class} \alias{class:RestContainer} \alias{RestContainer-class} % Constructor: \alias{RestContainer} % CRUD: \alias{[,RestContainer-method} \alias{[<-,RestContainer-method} \alias{$,RestContainer-method} \alias{[[,RestContainer-method} \alias{$<-,RestContainer-method} \alias{[[<-,RestContainer-method} % Show: \alias{show,RestContainer-method} \title{RestContainer} \description{ The \code{RestContainer} object wraps a collection of resources with a list-like interface. Values are stored and retrieved using familiar accessors like \code{[[} and \code{[[<-}. Coercion between external media and \R objects is based on the \code{\linkS4class{Media}} framework. } \section{Data access}{ The \code{RestContainer} object maps familiar R list accessors to CRUD operations on \code{\linkS4class{RestUri}}. \describe{ \item{}{ \code{x[] <- value}: Creates resources for the objects in \code{value} at \code{x}. This is the \code{\link{create}}/\code{POST} operation. Unlike an R list, the resources are added to the collection without removing any existing resources. This inconsistency is unfortunate, so we might change this behavior in the future. } \item{}{ \code{x$name}, \code{x[[i]]}: Reads the named element. This is the \code{\link{read}}/\code{GET} operation. } \item{}{ \code{x[i]}: Reads the named elements, which are returned in a list. This is the vectorized \code{\link{read}}/\code{GET} operation. Unlike an R list, this is not an endomorphism, in that the return value is dropped to a list and is no longer attached to the REST interface. } \item{}{ \code{x$name <- value}, \code{x[[i]] <- value}: Updates the named resource with \code{value}. This is the \code{\link{update}}/\code{PUT} operation. } \item{}{ \code{x[i] <- value}: Updates resources at \code{x} with the objects in \code{value}, a list. This is the vectorized \code{\link{update}}/\code{PUT} operation. } \item{}{ \code{x$name <- NULL}, \code{x[[i]] <- NULL}: Deletes the named resource. This is the \code{\link{delete}}/\code{DELETE} operation. } } } \section{Constructor}{ \describe{ \item{}{\code{RestContainer(...)}: Constructs an instance based on \code{\link{RestUri}(...)}. } } } \examples{ apache <- RestContainer("http://wiki.apache.org") apache$solr } \seealso{ \code{\linkS4class{RestUri}}, which is a lower-level but perhaps more sensible interface. } \author{ Michael Lawrence } \keyword{methods} \keyword{classes}