1 // Written in the D programming language 2 3 /** 4 IronCache service wrapper by curl 5 6 See_also 7 - $(LINK http://dev.iron.io/cache/reference/api/) 8 9 License: $(LINK2 http://boost.org/LICENSE_1_0.txt, Boost License 1.0) 10 Authors: karronoli 11 Copyright: karronoli 2015- 12 Date: 2015-Feb-15 13 14 Examples: 15 --- 16 import iron.cache; 17 const prjid = "...", token = "..."; 18 auto iron = new IronCache(prjid, token); 19 iron.put("myname", "mykey", "myvalue"); 20 JSONValue json = iron.get("myname", "mykey"); 21 assert(json["value"].str == "myvalue"); 22 --- 23 */ 24 25 module iron.cache; 26 import core.time; 27 import std.json; 28 import curl = std.net.curl; 29 import std.string : format; 30 import std.uri : encodeComponent; 31 import std.exception : collectException; 32 debug { 33 import std.stdio; 34 } 35 36 class IronCache 37 { 38 protected string base; 39 protected curl.HTTP delegate() client; 40 static const DEFAULT_HOST = "https://cache-aws-us-east-1.iron.io:443"; 41 static const DEFAULT_API_VERSION = "1"; 42 static const DEFAULT_TIMEOUT = 3.seconds; 43 /// See_Also: http://dev.iron.io/cache/reference/environment/ 44 static const MAX_KEY_LENGTH = 250; 45 static const MAX_VALUE_SIZE = 1000000; 46 47 @trusted 48 this(in string projectId, in string token, in string host = null) nothrow 49 { 50 this.base = (host? host: DEFAULT_HOST) 51 ~ '/' ~ DEFAULT_API_VERSION 52 ~ "/projects/" ~ projectId ~ "/caches"; 53 this.client = () { 54 auto curl = curl.HTTP(); 55 curl.operationTimeout(DEFAULT_TIMEOUT); 56 curl.addRequestHeader("Content-Type", "application/json; charset=utf-8"); 57 curl.addRequestHeader("Authorization", "OAuth " ~ token); 58 return curl; 59 }; 60 } 61 62 /** 63 * constructor for iron.json configuration. 64 * Throws: FileException or JSONException without valid file. 65 */ 66 @trusted 67 this(in string _path = null) 68 { 69 import std.file : isFile, readText; 70 const path = (_path != null && _path.isFile)? _path: "iron.json"; 71 auto config = parseJSON(readText(path)); 72 auto host = ("host" in config)? config["host"].str: null; 73 this(config["project_id"].str, config["token"].str, host); 74 } 75 76 unittest 77 { 78 import std.file : FileException; 79 assert(new IronCache()); 80 assert(collectException!FileException(new IronCache("none.json"))); 81 auto iron = new IronCache("badprjid", "badtoken"); 82 auto e = collectException!ICSException(iron.caches()); 83 assert(e.status.code == 404, e.toString); 84 auto name = "badname"; 85 assert(collectException!ICSException(iron.caches(name))); 86 assert(collectException!ICSException(iron.caches(name))); 87 assert(collectException!ICSException(iron.clear(name))); 88 assert(collectException!ICSException(iron.put(name, "k", "v"))); 89 assert(collectException!ICSException(iron.get(name, "k"))); 90 assert(collectException!ICSException(iron.increment(name, "k", 1))); 91 assert(collectException!ICSException(iron.remove(name, "k"))); 92 assert(collectException!ICSException(iron.remove(name))); 93 } 94 95 /** 96 * Throws: 97 * IronCacheStatusException(bad network) 98 * JSONException(bad response) 99 * Returns: http://dev.iron.io/cache/reference/api/#list_caches 100 */ 101 @trusted 102 public JSONValue caches(in uint page = 0) 103 { 104 const url = this.base ~ format("?page=%d", page); 105 auto c = this.client(); 106 string res; 107 if (auto e = collectException 108 (cast(string)curl.get!(curl.HTTP, ubyte)(url, c), res)) 109 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 110 debug { 111 stderr.writefln("[iron]%s: %s", __FUNCTION__, res); 112 } 113 return parseJSON(res); 114 } 115 116 /** 117 * Throws: 118 * IronCacheStatusException(bad network) 119 * JSONException(bad response) 120 * Returns: http://dev.iron.io/cache/reference/api/#get_info_about_a_cache 121 */ 122 @trusted 123 public JSONValue caches(in string name) 124 { 125 const url = this.base ~ '/' ~ encodeComponent(name); 126 auto c = this.client(); 127 string res; 128 if (auto e = collectException 129 (cast(string)curl.get!(curl.HTTP, ubyte)(url, c), res)) 130 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 131 debug { 132 stderr.writefln("[iron]%s: %s", __FUNCTION__, res); 133 } 134 return parseJSON(res); 135 } 136 137 /// Throws: IronCacheStatusException(bad network) 138 @trusted 139 public bool clear(in string name) 140 { 141 const url = this.base ~ format("/%s/clear", encodeComponent(name)); 142 auto c = this.client(); 143 char[] res; 144 if (auto e = collectException(curl.post(url, "", c), res)) 145 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 146 debug { 147 stderr.writefln("[iron]%s: %s", __FUNCTION__, res); 148 } 149 return true; 150 } 151 152 /// Throws: IronCacheStatusException(bad network) 153 @trusted 154 public bool put(in string name, in string key, in string value) 155 in { 156 assert(key.length <= MAX_KEY_LENGTH); 157 assert(value.length <= MAX_VALUE_SIZE); 158 } 159 body { 160 const url = this.base 161 ~ format("/%s/items/%s", encodeComponent(name), encodeComponent(key)); 162 auto json = JSONValue(["value" : JSONValue(value)]); 163 return this.put(name, key, json); 164 } 165 166 /// Throws: IronCacheStatusException(bad network) 167 @trusted 168 public bool put(in string name, in string key, in JSONValue json) 169 in { 170 assert(key.length <= MAX_KEY_LENGTH); 171 assert("value" in json.object); 172 } 173 body { 174 const url = this.base 175 ~ format("/%s/items/%s", encodeComponent(name), encodeComponent(key)); 176 auto c = this.client(); 177 char[] res; 178 if (auto e = collectException(curl.put(url, json.toString, c), res)) 179 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 180 debug { 181 stderr.writefln("[iron]%s: %s", __FUNCTION__, res); 182 } 183 return true; 184 } 185 186 /** 187 * Throws: 188 * IronCacheStatusException(bad network) 189 * JSONException(bad response) 190 * Returns: http://dev.iron.io/cache/reference/api/#get_an_item_from_a_cache 191 */ 192 @trusted 193 public JSONValue get(in string name, in string key) 194 { 195 const url = this.base 196 ~ format("/%s/items/%s", encodeComponent(name), encodeComponent(key)); 197 auto c = this.client(); 198 string res; 199 if (auto e = collectException 200 (cast(string)curl.get!(curl.HTTP, ubyte)(url, c), res)) 201 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 202 debug { 203 stderr.writefln("[iron]%s: %s", __FUNCTION__, res); 204 } 205 return parseJSON(res); 206 } 207 208 /// Throws: IronCacheStatusException(bad network) 209 @trusted 210 public bool increment(in string name, in string key, in int amount) 211 { 212 const url = this.base 213 ~ format("/%s/items/%s/increment", 214 encodeComponent(name), encodeComponent(key)); 215 auto json = JSONValue(["amount" : JSONValue(amount)]); 216 auto c = this.client(); 217 char[] res; 218 if (auto e = collectException(curl.post(url, json.toString, c), res)) 219 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 220 debug { 221 stderr.writefln("[iron]%s: %s", __FUNCTION__, res); 222 } 223 return true; 224 } 225 226 /// Throws: IronCacheStatusException(bad network) 227 @trusted 228 public bool remove(in string name, in string key) 229 { 230 const url = this.base 231 ~ format("/%s/items/%s", encodeComponent(name), encodeComponent(key)); 232 auto c = this.client(); 233 if (auto e = collectException(curl.del(url, c))) 234 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 235 return true; 236 } 237 238 /// Throws: IronCacheStatusException(bad network) 239 @trusted 240 public bool remove(in string name) 241 { 242 const url = this.base ~ '/' ~ encodeComponent(name); 243 auto c = this.client(); 244 if (auto e = collectException(curl.del(url, c))) 245 throw new ICSException(c.statusLine, __FILE__, __LINE__, e); 246 return true; 247 } 248 } 249 250 class IronCacheException : Exception 251 { 252 @safe pure nothrow 253 this(string msg, 254 string file = __FILE__, 255 size_t line = __LINE__, 256 Throwable next = null) 257 { 258 super(msg, file, line, next); 259 } 260 } 261 262 class IronCacheStatusException : IronCacheException 263 { 264 protected curl.HTTP.StatusLine _status; 265 @safe pure nothrow 266 @property curl.HTTP.StatusLine status() const {return this._status;} 267 268 @trusted 269 this(curl.HTTP.StatusLine status, 270 string file = __FILE__, 271 size_t line = __LINE__, 272 Throwable next = null) 273 { 274 this._status = status; 275 super(status.toString(), file, line, next); 276 } 277 } 278 alias ICSException = IronCacheStatusException; 279 280 unittest 281 { 282 auto name = { 283 import std.random, std.ascii, std.conv; 284 auto n = "名前", rndGen = Random(unpredictableSeed); 285 return n ~ randomSample(letters.dtext, 10, rndGen).text; 286 }(); 287 288 auto iron = new IronCache(); 289 auto e = collectException!IronCacheStatusException(iron.caches(name)); 290 assert(e.status.code == 404, e.toString); 291 292 const key1 = "鍵1", key2 = "鍵2", value = "値"; 293 assert(iron.put(name, key1, value)); 294 assert(iron.put(name, key2, JSONValue(["value": 1]))); 295 assert(iron.increment(name, key2, 2)); 296 assert(iron.increment(name, key2, -3)); 297 298 assert(iron.get(name, key1)["value"].str == value); 299 assert(iron.get(name, key2)["value"].integer == 0); 300 assert(iron.caches(name)["size"].integer == 2); 301 import std.algorithm : filter; 302 assert(! iron.caches().array.filter!(a => a["name"].str == name).empty); 303 304 assert(iron.remove(name, key1)); 305 assert(iron.caches(name)["size"].integer == 1); 306 assert(iron.clear(name)); 307 assert(iron.caches(name)["size"].integer == 0); 308 assert(iron.remove(name)); 309 assert(collectException!IronCacheStatusException(iron.caches(name))); 310 }