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 }