1 module couched; 2 import heaploop.networking.http; 3 import http.parser.core; 4 import std.conv : to; 5 import std.stdio : writeln; 6 import std.string : format; 7 import std.exception : enforceEx; 8 import medea; 9 10 private: 11 12 void checkResponse(ObjectValue resp, string file = __FILE__, size_t line = __LINE__) { 13 StringValue errorValue; 14 if("error" in resp) { 15 errorValue = cast(StringValue)resp["error"]; 16 } 17 if(errorValue) { 18 string errorString = errorValue.text; 19 string reasonString; 20 StringValue reasonValue; 21 if("reason" in resp) { 22 reasonValue = cast(StringValue)resp["reason"]; 23 } 24 if(reasonValue) { 25 reasonString = reasonValue.text; 26 } 27 CouchedError error; 28 switch(errorString) { 29 case "not_found": 30 error = CouchedError.NotFound; 31 break; 32 default: 33 error = CouchedError.Unknown; 34 break; 35 } 36 throw new CouchedException("%s: %s".format(errorString, reasonString), error, file, line); 37 } 38 } 39 /* 40 void checkJSONType(string source, JSON_TYPE type)(ref const JSONValue value, string file = __FILE__, size_t line = __LINE__) { 41 if(value.type != type) { 42 throw new CouchedException(source ~ " is not of type JSON_TYPE." ~ type.to!string, CouchedError.UnexpectedType, file, line); 43 } 44 } 45 46 JSONValue getJSONProperty(string source, string propertyName, JSON_TYPE type)(ref const JSONValue value, string file = __FILE__, size_t line = __LINE__) { 47 value.checkJSONType!(source, JSON_TYPE.OBJECT)(file, line); 48 if(propertyName !in value.object) { 49 throw new CouchedException(source ~ " is missing property " ~ propertyName, CouchedError.MissingProperty, file, line); 50 } 51 JSONValue prop = value.object[propertyName]; 52 prop.checkJSONType!(propertyName, JSON_TYPE.STRING)(file, line); 53 return prop; 54 } 55 */ 56 public: 57 58 enum CouchedError { 59 None, 60 Unknown, 61 NotFound, 62 InvalidDocument, 63 UnexpectedType, 64 MissingProperty 65 } 66 67 class CouchedException : Exception 68 { 69 private: 70 CouchedError _error; 71 72 public: 73 this(string msg, CouchedError error = CouchedError.Unknown, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 74 super(msg, file, line, next); 75 _error = error; 76 } 77 78 @property CouchedError error() pure nothrow { 79 return _error; 80 } 81 82 override string toString() { 83 return std..string.format("%s: %s", this.error.to!string, this.msg); 84 } 85 86 } 87 88 class CouchedDatabaseManager { 89 private: 90 CouchedClient _client; 91 92 package: 93 this(CouchedClient client) { 94 _client = client; 95 } 96 97 public: 98 void ensure(string name) { 99 _client._client.put("/" ~ name); 100 } 101 102 CouchedDatabase opIndex(string name) 103 in { 104 assert(name, "name is required"); 105 } 106 body { 107 return new CouchedDatabase(_client, name); 108 } 109 110 CouchedDatabase opDispatch(string name)() 111 { 112 return this[name]; 113 } 114 } 115 116 class CouchedDatabase { 117 private: 118 string _name; 119 CouchedClient _client; 120 121 string _documentPath(string uuid) { 122 return "/" ~ _name ~ "/" ~ uuid; 123 } 124 125 package: 126 this(CouchedClient client, string name) { 127 _client = client; 128 _name = name; 129 } 130 public: 131 ObjectValue update(ObjectValue value) { 132 if(!value) { 133 throw new CouchedException("Can't update null document", CouchedError.InvalidDocument); 134 } 135 string uuid; 136 if("_id" in value) { 137 StringValue idValue = cast(StringValue)value["_id"]; 138 uuid = idValue.text; 139 } else { 140 throw new CouchedException("document doesn't contain _id property", CouchedError.InvalidDocument); 141 } 142 return create(uuid, value); 143 } 144 145 ObjectValue create(string uuid, ObjectValue value) { 146 string valueText = value.toJSONString; 147 ubyte[] valueData = cast(ubyte[])valueText; 148 auto content = new UbyteContent(valueData); 149 auto response = _client._client.put(_documentPath(uuid), content); 150 ubyte[] data; 151 response.read ^= (chunk) { 152 data ~= chunk.buffer; 153 }; 154 string res = cast(string)data; 155 ObjectValue resp = cast(ObjectValue)res.parse; 156 resp.checkResponse(); 157 value["_rev"] = resp["rev"]; 158 value["_id"] = resp["id"]; 159 return resp; 160 } 161 162 ObjectValue delete_(ObjectValue value) { 163 if(!!value) { 164 throw new CouchedException("Can't update null document", CouchedError.InvalidDocument); 165 } 166 StringValue uuidObject = cast(StringValue)value["_id"]; 167 string uuid = uuidObject.text; 168 169 StringValue revIdObject = cast(StringValue)value["_rev"]; 170 string revId = revIdObject.text; 171 172 auto response = _client._client.send("DELETE", _documentPath(uuid) ~ "?rev=" ~ revId); 173 ubyte[] data; 174 response.read ^= (chunk) { 175 data ~= chunk.buffer; 176 }; 177 string res = cast(string)data; 178 ObjectValue resp = cast(ObjectValue)res.parse; 179 resp.checkResponse(); 180 return resp; 181 } 182 183 ObjectValue get(string uuid) { 184 auto response = _client._client.get(_documentPath(uuid)); 185 ubyte[] data; 186 response.read ^= (chunk) { 187 data ~= chunk.buffer; 188 }; 189 string res = cast(string)data; 190 ObjectValue resp = cast(ObjectValue)res.parse; 191 resp.checkResponse(); 192 return resp; 193 } 194 195 void ensure() { 196 _client.databases.ensure(_name); 197 } 198 } 199 200 class CouchedClient { 201 private: 202 Uri _uri; 203 CouchedDatabaseManager _databases; 204 205 package: 206 HttpClient _client; 207 208 public: 209 this(string uri) { 210 this(Uri(uri)); 211 } 212 this(Uri uri) { 213 _uri = uri; 214 _client = new HttpClient(_uri); 215 _databases = new CouchedDatabaseManager(this); 216 } 217 218 @property { 219 Uri uri() nothrow pure { 220 return _uri; 221 } 222 223 CouchedDatabaseManager databases() nothrow pure { 224 return _databases; 225 } 226 227 } 228 }