pike.git
/
lib
/
modules
/
WebApi.pmod
/
Api.pike
version
»
Context lines:
10
20
40
80
file
none
3
pike.git/lib/modules/WebApi.pmod/Api.pike:1:
+
//! Base class for implementing a @tt{(RESTful) WebApi@} like Facebook's
+
//! Graph API, Instagram's API, Twitter's API and so on.
+
//!
+
//! @b{Note:@} This class is useless in it self, and is intended to be
+
//! inherited by classes implementing a given @tt{WebApi@}.
+
//!
+
//! Look at the code in @[WebApi.Github], @[WebApi.Instagram],
+
//! @[WebApi.Linkedin] etc to see some examples of implementations.
+
+
#if defined(SOCIAL_REQUEST_DEBUG) || defined(SOCIAL_REQUEST_DATA_DEBUG)
+
# define TRACE(X...) werror("%s:%d: %s",basename(__FILE__),__LINE__,sprintf(X))
+
#else
+
# define TRACE(X...) 0
+
#endif
+
+
//! The URI to the remote API
+
constant API_URI = 0;
+
+
//! In some API's (LinkedIn f ex) this is named something else so it needs
+
//! to be overridden i cases where it has a different name than the
+
//! standard one
+
protected constant ACCESS_TOKEN_PARAM_NAME = "access_token";
+
+
//! If @expr{1@} @[Standards.JSON.decode_utf8()] will be used when JSON data
+
//! is decoded.
+
protected constant DECODE_UTF8 = 0;
+
+
//! If @expr{1@} @[Standards.JSON.decode_utf8()] will be used when JSON data
+
//! is decoded.
+
public int(0..1) utf8_decode = DECODE_UTF8;
+
+
//! Typedef for the async callback method signature.
+
typedef function(mapping,Protocols.HTTP.Query:void) Callback;
+
+
//! Typef for a parameter argument
+
typedef mapping|Auth.Params ParamsArg;
+
+
//! Authorization object.
+
//!
+
//! @seealso
+
//! @[Auth.OAuth2]
+
protected Auth.OAuth2.Client _auth;
+
+
//! Authentication class to use
+
protected constant AuthClass = Auth.OAuth2.Client;
+
+
protected mapping(string:string) default_headers = ([
+
"user-agent" : .USER_AGENT
+
]);
+
+
//! Creates a new Api instance
+
//!
+
//! @param client_id
+
//! The application ID
+
//!
+
//! @param client_secret
+
//! The application secret
+
//!
+
//! @param redirect_uri
+
//! Where the authorization page should redirect back to. This must be
+
//! fully qualified domain name.
+
//!
+
//! @param scope
+
//! Extended permissions to use for this authentication.
+
protected void create(string client_id, string client_secret,
+
void|string redirect_uri,
+
void|string|array(string)|multiset(string) scope)
+
{
+
if (AuthClass)
+
_auth = AuthClass(client_id, client_secret, redirect_uri, scope);
+
}
+
+
//! Getter for the authentication object. Most likely this will be a class
+
//! derived from @[Auth.OAuth2.Client].
+
//!
+
//! @seealso
+
//! @[Auth.OAuth2.Client] or @[Auth.OAuth.Client]
+
Auth.OAuth2.Client `auth()
+
{
+
return _auth;
+
}
+
+
//! This can be used to parse a link resource returned from a REST api.
+
//! Many API returns stuff like:
+
//!
+
//! @code
+
//! {
+
//! ...
+
//! "pagination" : {
+
//! "next" : "/api/v1/method/path?some=variables&page=2&per_page=20"
+
//! }
+
//! }
+
//! @endcode
+
//!
+
//! If @tt{pagination->next@} is passed to this method it will return a path
+
//! of @tt{/method/path@}, given that the base URI of the web api is
+
//! something along the line of @tt{https://some.url/api/v1@}, and a
+
//! mapping containing the query variables (which can be passed as a parameter
+
//! to any of the @tt{get, post, delete, put@} methods.
+
//!
+
//! @param url
+
//!
+
//! @returns
+
//! @mapping
+
//! @member string "path"
+
//! @member mapping "params"
+
//! @endmapping
+
mapping(string:string|mapping) parse_canonical_url(string url)
+
{
+
Standards.URI api_uri = Standards.URI(API_URI);
+
Standards.URI this_url;
+
+
// The links returned from a REST api doesn't always contain the
+
// domain name. If not we catch it and set localhost as domain since
+
// we only want the path and variables
+
if (catch(this_url = Standards.URI(url))) {
+
this_url = Standards.URI("http://localhost" + url);
+
}
+
+
mapping params = this_url->get_query_variables() || ([]);
+
+
foreach (indices(params), string k) {
+
if (!sizeof(params[k]))
+
m_delete(params, k);
+
}
+
+
string path = this_url->path;
+
+
if (has_prefix(path, api_uri->path))
+
path -= api_uri->path;
+
+
return ([ "path" : path, "params" : params ]);
+
}
+
+
//! Invokes a call with a GET method
+
//!
+
//! @param api_method
+
//! The remote API method to call
+
//! @param params
+
//! @param cb
+
//! Callback function to get into in async mode
+
mixed get(string api_method, void|ParamsArg params, void|Callback cb)
+
{
+
return call(api_method, params, "GET", 0, cb);
+
}
+
+
//! Invokes a call with a POST method
+
//!
+
//! @param api_method
+
//! The remote API method to call
+
//! @param params
+
//! @param data
+
//! Eventual inline data to send
+
//! @param cb
+
//! Callback function to get into in async mode
+
mixed post(string api_method, void|ParamsArg params, void|string data,
+
void|Callback cb)
+
{
+
return call(api_method, params, "POST", data, cb);
+
}
+
+
//! Invokes a call with a DELETE method
+
//!
+
//! @param api_method
+
//! The remote API method to call
+
//! @param params
+
//! @param cb
+
//! Callback function to get into in async mode
+
mixed delete(string api_method, void|ParamsArg params, void|Callback cb)
+
{
+
return call(api_method, params, "DELETE", 0, cb);
+
}
+
+
//! Invokes a call with a PUT method
+
//!
+
//! @param api_method
+
//! The remote API method to call
+
//! @param params
+
//! @param cb
+
//! Callback function to get into in async mode
+
mixed put(string api_method, void|ParamsArg params, void|Callback cb)
+
{
+
return call(api_method, params, "PUT", 0, cb);
+
}
+
+
//! Invokes a call with a PATCH method
+
//!
+
//! @param api_method
+
//! The remote API method to call
+
//! @param params
+
//! @param cb
+
//! Callback function to get into in async mode
+
mixed patch(string api_method, void|ParamsArg params, void|Callback cb)
+
{
+
return call(api_method, params, "PATCH", 0, cb);
+
}
+
+
//! Calls a remote API method.
+
//!
+
//! @throws
+
//! An exception is thrown if the response status code is other than
+
//! @expr{200@}, @expr{301@} or @expr{302@}.
+
//!
+
//! @param api_method
+
//! The remote API method to call!
+
//! This should be a Fully Qualified Domain Name
+
//!
+
//! @param params
+
//! Additional params to send in the request
+
//!
+
//! @param http_method
+
//! HTTP method to use. @expr{GET@} is default
+
//!
+
//! @param data
+
//! Inline data to send in a @expr{POST@} request for instance.
+
//!
+
//! @param cb
+
//! Callback function to get into in async mode
+
//!
+
//! @returns
+
//! If JSON is available the JSON response from servie will be decoded
+
//! and returned. If not, the raw response (e.g a JSON string) will be
+
//! returned. The exception to this is if the status code in the response is a
+
//! @expr{30x@} (a redirect), then the response headers mapping will be
+
//! returned.
+
mixed call(string api_method, void|ParamsArg params,
+
void|string http_method, void|string data, void|Callback cb)
+
{
+
http_method = upper_case(http_method || "get");
+
Auth.Params p = Auth.Params();
+
p->add_mapping(default_params());
+
+
if (params) p += params;
+
+
if (_auth && !_auth->is_expired()) {
+
if (string a = _auth->access_token) {
+
p += Auth.Param(ACCESS_TOKEN_PARAM_NAME, a);
+
}
+
}
+
+
if (http_method == "POST") {
+
if (!data) data = (string) p;
+
params = 0;
+
}
+
else {
+
params = (mapping) p;
+
}
+
+
//Request req;
+
Protocols.HTTP.Query req = Protocols.HTTP.Query();
+
+
#ifdef SOCIAL_REQUEST_DEBUG
+
TRACE("\n> Request: %s %s?%s\n", http_method, api_method, (string) p);
+
if (data) TRACE("> data: %s\n", data);
+
#endif
+
+
if (cb) {
+
req->set_callbacks(
+
lambda (Protocols.HTTP.Query qq, mixed ... args) {
+
if (qq->status == 200) {
+
cb(handle_response(qq), qq);
+
}
+
},
+
lambda (Protocols.HTTP.Query qq, mixed ... args) {
+
cb(0, qq);
+
}
+
);
+
+
Protocols.HTTP.do_async_method(http_method, api_method, params,
+
default_headers, req, data);
+
+
+
return 0;
+
}
+
+
req = Protocols.HTTP.do_method(http_method, api_method, params,
+
default_headers, req, data);
+
return req && handle_response(req);
+
}
+
+
private mixed handle_response(Protocols.HTTP.Query req)
+
{
+
TRACE("Handle response: %O\n", req);
+
+
if ((< 301, 302 >)[req->status])
+
return req->headers;
+
+
#ifdef SOCIAL_REQUEST_DATA_DEBUG
+
TRACE("Data: [%s]\n\n", req->data()||"(empty)");
+
#endif
+
+
if (req->status != 200) {
+
string d = req->data();
+
+
TRACE("Bad resp[%d]: %s\n\n%O\n",
+
req->status, req->data(), req->headers);
+
+
if (has_value(d, "error")) {
+
mapping e;
+
mixed err = catch {
+
e = Standards.JSON.decode(d);
+
};
+
+
if (e) {
+
if (e->error)
+
error("Error %d: %s. ", e->error->code, e->error->message);
+
else if (e->meta && e->meta->code)
+
error("Error %d: %s. ", e->meta->code, e->meta->error_message);
+
}
+
+
error("Error: %s", "Unknown");
+
}
+
+
error("Bad status (%d) in HTTP response! ", req->status);
+
}
+
+
if (utf8_decode) {
+
TRACE("Decode UTF8: %s\n", req->data());
+
return Standards.JSON.decode_utf8(unescape_forward_slashes(req->data()));
+
}
+
+
return Standards.JSON.decode(unescape_forward_slashes(req->data()));
+
}
+
+
//! String format
+
//!
+
//! @param t
+
string _sprintf(int t)
+
{
+
return sprintf("%O(authorized:%O)", this_program,
+
(_auth && !!_auth->access_token));
+
}
+
+
//! Convenience method for getting the URI to a specific API method
+
//!
+
//! @param method
+
protected string get_uri(string method)
+
{
+
if (has_suffix(API_URI, "/")) {
+
if (has_prefix(method, "/"))
+
method = method[1..];
+
}
+
else {
+
if (!has_prefix(method, "/"))
+
method = "/" + method;
+
}
+
+
// Remove eventual double slashes.
+
if (search(method, "//") > -1)
+
method = replace(method, "//", "/");
+
+
return API_URI + method;
+
}
+
+
//! Returns the encoding from a response
+
//!
+
//! @param h
+
//! The headers mapping from a HTTP respose
+
protected string get_encoding(mapping h)
+
{
+
if (h["content-type"]) {
+
sscanf(h["content-type"], "%*scharset=%s", string s);
+
return s && lower_case(String.trim_all_whites(s)) || "";
+
}
+
+
return "";
+
}
+
+
//! Unescapes escaped forward slashes in a JSON string
+
protected string unescape_forward_slashes(string s)
+
{
+
return replace(s, "\\/", "/");
+
}
+
+
//! Return default params
+
protected mapping default_params()
+
{
+
return ([]);
+
}
+
+
//! Internal class ment to be inherited by implementing Api's classes that
+
//! corresponds to a given API endpoint.
+
class Method
+
{
+
//! API method location within the API
+
//!
+
//! @code
+
//! https://api.instagram.com/v1/media/search
+
//! ............................^^^^^^^
+
//! @endcode
+
protected constant METHOD_PATH = 0;
+
+
//! Hidden constructor. This class can not be instantiated directly
+
protected void create()
+
{
+
if (this_program == WebApi.Api.Method)
+
error("This class can not be instantiated directly! ");
+
}
+
+
//! Internal convenience method
+
protected mixed _get(string s, void|ParamsArg p, void|Callback cb);
+
+
//! Internal convenience method
+
protected mixed _put(string s, void|ParamsArg p, void|Callback cb);
+
+
//! Internal convenience method
+
protected mixed _post(string s, void|ParamsArg p, void|string data,
+
void|Callback cb);
+
+
//! Internal convenience method
+
protected mixed _delete(string s, void|ParamsArg p, void|Callback cb);
+
+
//! Internal convenience method
+
protected mixed _patch(string s, void|ParamsArg p, void|Callback cb);
+
}
Newline at end of file added.