streamly_ffi 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ext/streamly.c ADDED
@@ -0,0 +1,427 @@
1
+ // static size_t upload_data_handler(char * stream, size_t size, size_t nmemb, VALUE upload_stream) {
2
+ // size_t result = 0;
3
+ //
4
+ // // TODO
5
+ // // Change upload_stream back to a VALUE
6
+ // // if TYPE(upload_stream) == T_STRING - read at most "len" continuously
7
+ // // if upload_stream is IO-like, read chunks of it
8
+ // // OR
9
+ // // if upload_stream responds to "each", use that?
10
+ //
11
+ // TRAP_BEG;
12
+ // // if (upload_stream != NULL && *upload_stream != NULL) {
13
+ // // int len = size * nmemb;
14
+ // // char *s1 = strncpy(stream, *upload_stream, len);
15
+ // // result = strlen(s1);
16
+ // // *upload_stream += result;
17
+ // // }
18
+ // TRAP_END;
19
+ //
20
+ // return result;
21
+ // }
22
+
23
+ #include "streamly.h"
24
+ #ifdef HAVE_RUBY_ENCODING_H
25
+ #include <ruby/encoding.h>
26
+ static rb_encoding *utf8Encoding;
27
+ #endif
28
+
29
+ static size_t header_handler(char * stream, size_t size, size_t nmemb, VALUE handler) {
30
+ size_t str_len = size * nmemb;
31
+
32
+ if(TYPE(handler) == T_STRING) {
33
+ #ifdef HAVE_RUBY_ENCODING_H
34
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
35
+ if (default_internal_enc) {
36
+ handler = rb_str_export_to_enc(handler, default_internal_enc);
37
+ } else {
38
+ handler = rb_str_export_to_enc(handler, utf8Encoding);
39
+ }
40
+ #endif
41
+ rb_str_buf_cat(handler, stream, str_len);
42
+ } else {
43
+ VALUE chunk = rb_str_new(stream, str_len);
44
+ #ifdef HAVE_RUBY_ENCODING_H
45
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
46
+ if (default_internal_enc) {
47
+ chunk = rb_str_export_to_enc(chunk, default_internal_enc);
48
+ } else {
49
+ chunk = rb_str_export_to_enc(chunk, utf8Encoding);
50
+ }
51
+ #endif
52
+ rb_funcall(handler, rb_intern("call"), 1, chunk);
53
+ }
54
+ return str_len;
55
+ }
56
+
57
+ static size_t data_handler(char * stream, size_t size, size_t nmemb, VALUE handler) {
58
+ size_t str_len = size * nmemb;
59
+
60
+ if(TYPE(handler) == T_STRING) {
61
+ #ifdef HAVE_RUBY_ENCODING_H
62
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
63
+ if (default_internal_enc) {
64
+ handler = rb_str_export_to_enc(handler, default_internal_enc);
65
+ } else {
66
+ handler = rb_str_export_to_enc(handler, utf8Encoding);
67
+ }
68
+ #endif
69
+ rb_str_buf_cat(handler, stream, str_len);
70
+ } else {
71
+ VALUE chunk = rb_str_new(stream, str_len);
72
+ #ifdef HAVE_RUBY_ENCODING_H
73
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
74
+ if (default_internal_enc) {
75
+ chunk = rb_str_export_to_enc(chunk, default_internal_enc);
76
+ } else {
77
+ chunk = rb_str_export_to_enc(chunk, utf8Encoding);
78
+ }
79
+ #endif
80
+ rb_funcall(handler, rb_intern("call"), 1, chunk);
81
+ }
82
+ return str_len;
83
+ }
84
+
85
+ static void streamly_instance_mark(struct curl_instance * instance) {
86
+ rb_gc_mark(instance->request_method);
87
+ rb_gc_mark(instance->request_payload_handler);
88
+ rb_gc_mark(instance->response_header_handler);
89
+ rb_gc_mark(instance->response_body_handler);
90
+ rb_gc_mark(instance->options);
91
+ }
92
+
93
+ static void streamly_instance_free(struct curl_instance * instance) {
94
+ curl_easy_cleanup(instance->handle);
95
+ free(instance);
96
+ }
97
+
98
+ // Initially borrowed from Patron - http://github.com/toland/patron
99
+ // slightly modified for Streamly
100
+ static VALUE each_http_header(VALUE header, VALUE self) {
101
+ struct curl_instance * instance;
102
+ GetInstance(self, instance);
103
+ size_t key_len, val_len, header_str_len;
104
+ VALUE key, val;
105
+
106
+ key = rb_ary_entry(header, 0);
107
+ key_len = RSTRING_LEN(key);
108
+
109
+ val = rb_ary_entry(header, 1);
110
+ val_len = RSTRING_LEN(val);
111
+
112
+ header_str_len = (key_len + val_len + 3);
113
+ unsigned char header_str[header_str_len];
114
+
115
+ memcpy(header_str, RSTRING_PTR(key), key_len);
116
+ memcpy(header_str+2, ": ", 2);
117
+ memcpy(header_str+val_len, RSTRING_PTR(val), val_len);
118
+
119
+ header_str[header_str_len+1] = '\0';
120
+ instance->request_headers = curl_slist_append(instance->request_headers, (char *)header_str);
121
+ return Qnil;
122
+ }
123
+
124
+ // Initially borrowed from Patron - http://github.com/toland/patron
125
+ // slightly modified for Streamly
126
+ static VALUE select_error(CURLcode code) {
127
+ VALUE error = Qnil;
128
+
129
+ switch (code) {
130
+ case CURLE_UNSUPPORTED_PROTOCOL:
131
+ error = eUnsupportedProtocol;
132
+ break;
133
+ case CURLE_URL_MALFORMAT:
134
+ error = eURLFormatError;
135
+ break;
136
+ case CURLE_COULDNT_RESOLVE_HOST:
137
+ error = eHostResolutionError;
138
+ break;
139
+ case CURLE_COULDNT_CONNECT:
140
+ error = eConnectionFailed;
141
+ break;
142
+ case CURLE_PARTIAL_FILE:
143
+ error = ePartialFileError;
144
+ break;
145
+ case CURLE_OPERATION_TIMEDOUT:
146
+ error = eTimeoutError;
147
+ break;
148
+ case CURLE_TOO_MANY_REDIRECTS:
149
+ error = eTooManyRedirects;
150
+ break;
151
+ default:
152
+ error = eStreamlyError;
153
+ }
154
+
155
+ return error;
156
+ }
157
+
158
+ /*
159
+ * Document-class: Streamly::Request
160
+ *
161
+ * A streaming REST client for Ruby that uses libcurl to do the heavy lifting.
162
+ * The API is almost exactly like rest-client, so users of that library should find it very familiar.
163
+ */
164
+ /*
165
+ * Document-method: new
166
+ *
167
+ * call-seq: new(args)
168
+ *
169
+ * +args+ should be a Hash and is required
170
+ * This Hash should at least contain +:url+ and +:method+ keys.
171
+ * You may also provide the following optional keys:
172
+ * +:headers+ - should be a Hash of name/value pairs
173
+ * +:response_header_handler+ - can be a string or object that responds to #call
174
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
175
+ * +:response_body_handler+ - can be a string or object that responds to #call
176
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
177
+ * +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
178
+ *
179
+ */
180
+ static VALUE rb_streamly_new(int argc, VALUE * argv, VALUE klass) {
181
+ struct curl_instance * instance;
182
+ VALUE obj = Data_Make_Struct(klass, struct curl_instance, streamly_instance_mark, streamly_instance_free, instance);
183
+ rb_obj_call_init(obj, argc, argv);
184
+ return obj;
185
+ }
186
+
187
+ /*
188
+ * Document-method: initialize
189
+ *
190
+ * call-seq: initialize(args)
191
+ *
192
+ * +args+ should be a Hash and is required
193
+ * This Hash should at least contain +:url+ and +:method+ keys.
194
+ * You may also provide the following optional keys:
195
+ * +:headers+ - should be a Hash of name/value pairs
196
+ * +:response_header_handler+ - can be a string or object that responds to #call
197
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
198
+ * +:response_body_handler+ - can be a string or object that responds to #call
199
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
200
+ * +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
201
+ *
202
+ */
203
+ static VALUE rb_streamly_init(int argc, VALUE * argv, VALUE self) {
204
+ struct curl_instance * instance;
205
+ VALUE args, url, payload, headers, username, password, credentials;
206
+
207
+ GetInstance(self, instance);
208
+ instance->handle = curl_easy_init();
209
+ instance->request_headers = NULL;
210
+ instance->request_method = Qnil;
211
+ instance->request_payload_handler = Qnil;
212
+ instance->response_header_handler = Qnil;
213
+ instance->response_body_handler = Qnil;
214
+ instance->options = Qnil;
215
+
216
+ rb_scan_args(argc, argv, "10", &args);
217
+
218
+ // Ensure our args parameter is a hash
219
+ Check_Type(args, T_HASH);
220
+
221
+ instance->request_method = rb_hash_aref(args, sym_method);
222
+ url = rb_hash_aref(args, sym_url);
223
+ payload = rb_hash_aref(args, sym_payload);
224
+ headers = rb_hash_aref(args, sym_headers);
225
+ username = rb_hash_aref(args, sym_username);
226
+ password = rb_hash_aref(args, sym_password);
227
+ instance->response_header_handler = rb_hash_aref(args, sym_response_header_handler);
228
+ instance->response_body_handler = rb_hash_aref(args, sym_response_body_handler);
229
+
230
+ // First lets verify we have a :method key
231
+ if (NIL_P(instance->request_method)) {
232
+ rb_raise(eStreamlyError, "You must specify a :method");
233
+ } else {
234
+ // OK, a :method was specified, but if it's POST or PUT we require a :payload
235
+ if (instance->request_method == sym_post || instance->request_method == sym_put) {
236
+ if (NIL_P(payload)) {
237
+ rb_raise(eStreamlyError, "You must specify a :payload for POST and PUT requests");
238
+ }
239
+ }
240
+ }
241
+
242
+ // Now verify a :url was provided
243
+ if (NIL_P(url)) {
244
+ rb_raise(eStreamlyError, "You must specify a :url to request");
245
+ }
246
+
247
+ if (NIL_P(instance->response_header_handler)) {
248
+ instance->response_header_handler = rb_str_new2("");
249
+ #ifdef HAVE_RUBY_ENCODING_H
250
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
251
+ if (default_internal_enc) {
252
+ instance->response_header_handler = rb_str_export_to_enc(instance->response_header_handler, default_internal_enc);
253
+ } else {
254
+ instance->response_header_handler = rb_str_export_to_enc(instance->response_header_handler, utf8Encoding);
255
+ }
256
+ #endif
257
+ }
258
+ if (instance->request_method != sym_head && NIL_P(instance->response_body_handler)) {
259
+ instance->response_body_handler = rb_str_new2("");
260
+ #ifdef HAVE_RUBY_ENCODING_H
261
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
262
+ if (default_internal_enc) {
263
+ instance->response_body_handler = rb_str_export_to_enc(instance->response_body_handler, default_internal_enc);
264
+ } else {
265
+ instance->response_body_handler = rb_str_export_to_enc(instance->response_body_handler, utf8Encoding);
266
+ }
267
+ #endif
268
+ }
269
+
270
+ if (!NIL_P(headers)) {
271
+ Check_Type(headers, T_HASH);
272
+ rb_iterate(rb_each, headers, each_http_header, self);
273
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPHEADER, instance->request_headers);
274
+ }
275
+
276
+ // So far so good, lets start setting up our request
277
+
278
+ // Set the type of request
279
+ if (instance->request_method == sym_head) {
280
+ curl_easy_setopt(instance->handle, CURLOPT_NOBODY, 1);
281
+ } else if (instance->request_method == sym_get) {
282
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPGET, 1);
283
+ } else if (instance->request_method == sym_post) {
284
+ curl_easy_setopt(instance->handle, CURLOPT_POST, 1);
285
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
286
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
287
+
288
+ // (multipart)
289
+ // curl_easy_setopt(instance->handle, CURLOPT_HTTPPOST, 1);
290
+
291
+ // TODO: get streaming upload working
292
+ // curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &upload_data_handler);
293
+ // curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
294
+ // curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
295
+ } else if (instance->request_method == sym_put) {
296
+ curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "PUT");
297
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
298
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
299
+
300
+ // TODO: get streaming upload working
301
+ // curl_easy_setopt(instance->handle, CURLOPT_UPLOAD, 1);
302
+ // curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &upload_data_handler);
303
+ // curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
304
+ // curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
305
+ } else if (instance->request_method == sym_delete) {
306
+ curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "DELETE");
307
+ }
308
+
309
+ // Other common options
310
+ curl_easy_setopt(instance->handle, CURLOPT_URL, RSTRING_PTR(url));
311
+ curl_easy_setopt(instance->handle, CURLOPT_FOLLOWLOCATION, 1);
312
+ curl_easy_setopt(instance->handle, CURLOPT_MAXREDIRS, 3);
313
+
314
+ // Response header handling
315
+ curl_easy_setopt(instance->handle, CURLOPT_HEADERFUNCTION, &header_handler);
316
+ curl_easy_setopt(instance->handle, CURLOPT_HEADERDATA, instance->response_header_handler);
317
+
318
+ // Response body handling
319
+ if (instance->request_method != sym_head) {
320
+ curl_easy_setopt(instance->handle, CURLOPT_ENCODING, "identity, deflate, gzip");
321
+ curl_easy_setopt(instance->handle, CURLOPT_WRITEFUNCTION, &data_handler);
322
+ curl_easy_setopt(instance->handle, CURLOPT_WRITEDATA, instance->response_body_handler);
323
+ }
324
+
325
+ if (!NIL_P(username) || !NIL_P(password)) {
326
+ credentials = rb_str_new2("");
327
+ if (!NIL_P(username)) {
328
+ rb_str_buf_cat(credentials, RSTRING_PTR(username), RSTRING_LEN(username));
329
+ }
330
+ rb_str_buf_cat(credentials, ":", 1);
331
+ if (!NIL_P(password)) {
332
+ rb_str_buf_cat(credentials, RSTRING_PTR(password), RSTRING_LEN(password));
333
+ }
334
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC | CURLAUTH_DIGEST);
335
+ curl_easy_setopt(instance->handle, CURLOPT_USERPWD, RSTRING_PTR(credentials));
336
+ rb_gc_mark(credentials);
337
+ }
338
+
339
+ curl_easy_setopt(instance->handle, CURLOPT_SSL_VERIFYPEER, 0);
340
+ curl_easy_setopt(instance->handle, CURLOPT_SSL_VERIFYHOST, 0);
341
+
342
+ curl_easy_setopt(instance->handle, CURLOPT_ERRORBUFFER, instance->error_buffer);
343
+
344
+ return self;
345
+ }
346
+
347
+ static VALUE nogvl_perform(void *handle) {
348
+ CURLcode res;
349
+ VALUE status = Qnil;
350
+
351
+ res = curl_easy_perform(handle);
352
+ if (CURLE_OK != res) {
353
+ status = select_error(res);
354
+ }
355
+
356
+ return status;
357
+ }
358
+
359
+ /*
360
+ * Document-method: rb_streamly_execute
361
+ *
362
+ * call-seq: rb_streamly_execute
363
+ */
364
+ static VALUE rb_streamly_execute(RB_STREAMLY_UNUSED int argc, RB_STREAMLY_UNUSED VALUE * argv, VALUE self) {
365
+ VALUE status;
366
+ struct curl_instance * instance;
367
+ GetInstance(self, instance);
368
+
369
+ // Done setting up, lets do this!
370
+ status = rb_thread_blocking_region(nogvl_perform, instance->handle, RUBY_UBF_IO, 0);
371
+ if (!NIL_P(status)) {
372
+ rb_raise(status, "%s", instance->error_buffer);
373
+ }
374
+
375
+ // Cleanup
376
+ if (instance->request_headers != NULL) {
377
+ curl_slist_free_all(instance->request_headers);
378
+ instance->request_headers = NULL;
379
+ }
380
+ curl_easy_reset(instance->handle);
381
+ instance->request_payload_handler = Qnil;
382
+
383
+ if (instance->request_method == sym_head && TYPE(instance->response_header_handler) == T_STRING) {
384
+ return instance->response_header_handler;
385
+ } else if (TYPE(instance->response_body_handler) == T_STRING) {
386
+ return instance->response_body_handler;
387
+ } else {
388
+ return Qnil;
389
+ }
390
+ }
391
+
392
+ // Ruby Extension initializer
393
+ void Init_streamly_ext() {
394
+ mStreamly = rb_define_module("Streamly");
395
+
396
+ cRequest = rb_define_class_under(mStreamly, "Request", rb_cObject);
397
+ rb_define_singleton_method(cRequest, "new", rb_streamly_new, -1);
398
+ rb_define_method(cRequest, "initialize", rb_streamly_init, -1);
399
+ rb_define_method(cRequest, "execute", rb_streamly_execute, -1);
400
+
401
+ eStreamlyError = rb_define_class_under(mStreamly, "Error", rb_eStandardError);
402
+ eUnsupportedProtocol = rb_define_class_under(mStreamly, "UnsupportedProtocol", rb_eStandardError);
403
+ eURLFormatError = rb_define_class_under(mStreamly, "URLFormatError", rb_eStandardError);
404
+ eHostResolutionError = rb_define_class_under(mStreamly, "HostResolutionError", rb_eStandardError);
405
+ eConnectionFailed = rb_define_class_under(mStreamly, "ConnectionFailed", rb_eStandardError);
406
+ ePartialFileError = rb_define_class_under(mStreamly, "PartialFileError", rb_eStandardError);
407
+ eTimeoutError = rb_define_class_under(mStreamly, "TimeoutError", rb_eStandardError);
408
+ eTooManyRedirects = rb_define_class_under(mStreamly, "TooManyRedirects", rb_eStandardError);
409
+
410
+ sym_method = ID2SYM(rb_intern("method"));
411
+ sym_url = ID2SYM(rb_intern("url"));
412
+ sym_payload = ID2SYM(rb_intern("payload"));
413
+ sym_headers = ID2SYM(rb_intern("headers"));
414
+ sym_head = ID2SYM(rb_intern("head"));
415
+ sym_get = ID2SYM(rb_intern("get"));
416
+ sym_post = ID2SYM(rb_intern("post"));
417
+ sym_put = ID2SYM(rb_intern("put"));
418
+ sym_delete = ID2SYM(rb_intern("delete"));
419
+ sym_username = ID2SYM(rb_intern("username"));
420
+ sym_password = ID2SYM(rb_intern("password"));
421
+ sym_response_header_handler = ID2SYM(rb_intern("response_header_handler"));
422
+ sym_response_body_handler = ID2SYM(rb_intern("response_body_handler"));
423
+
424
+ #ifdef HAVE_RUBY_ENCODING_H
425
+ utf8Encoding = rb_utf8_encoding();
426
+ #endif
427
+ }
data/ext/streamly.h ADDED
@@ -0,0 +1,66 @@
1
+ #include <curl/curl.h>
2
+ #include <ruby.h>
3
+
4
+ VALUE mStreamly, cRequest, eStreamlyError, eUnsupportedProtocol, eURLFormatError, eHostResolutionError;
5
+ VALUE eConnectionFailed, ePartialFileError, eTimeoutError, eTooManyRedirects;
6
+ VALUE sym_method, sym_url, sym_payload, sym_headers, sym_head, sym_get, sym_post, sym_put, sym_delete;
7
+ VALUE sym_response_header_handler, sym_response_body_handler, sym_username, sym_password;
8
+
9
+ #define GetInstance(obj, sval) (sval = (struct curl_instance*)DATA_PTR(obj));
10
+
11
+ struct curl_instance {
12
+ CURL* handle;
13
+ char error_buffer[CURL_ERROR_SIZE];
14
+ struct curl_slist* request_headers;
15
+ VALUE request_payload_handler;
16
+ VALUE response_header_handler;
17
+ VALUE response_body_handler;
18
+ VALUE request_method;
19
+ VALUE options;
20
+ };
21
+
22
+ // libcurl callbacks
23
+ static size_t header_handler(char * stream, size_t size, size_t nmemb, VALUE handler);
24
+ static size_t data_handler(char * stream, size_t size, size_t nmemb, VALUE handler);
25
+ // static size_t put_data_handler(char * stream, size_t size, size_t nmemb, VALUE upload_stream);
26
+ //
27
+ static VALUE select_error(CURLcode code);
28
+ static VALUE each_http_header(VALUE header, VALUE header_array);
29
+ static void streamly_instance_mark(struct curl_instance * instance);
30
+ static void streamly_instance_free(struct curl_instance * instance);
31
+
32
+ static VALUE rb_streamly_new(int argc, VALUE * argv, VALUE klass);
33
+
34
+ #if defined(__GNUC__) && (__GNUC__ >= 3)
35
+ #define RB_STREAMLY_UNUSED __attribute__ ((unused))
36
+ #else
37
+ #define RB_STREAMLY_UNUSED
38
+ #endif
39
+
40
+ /*
41
+ * partial emulation of the 1.9 rb_thread_blocking_region under 1.8,
42
+ * this is enough for dealing with blocking I/O functions in the
43
+ * presence of threads.
44
+ */
45
+ #ifndef HAVE_RB_THREAD_BLOCKING_REGION
46
+
47
+ #include <rubysig.h>
48
+ #define RUBY_UBF_IO ((rb_unblock_function_t *)-1)
49
+ typedef void rb_unblock_function_t(void *);
50
+ typedef VALUE rb_blocking_function_t(void *);
51
+ static VALUE
52
+ rb_thread_blocking_region(
53
+ rb_blocking_function_t *func, void *data1,
54
+ RB_STREAMLY_UNUSED rb_unblock_function_t *ubf,
55
+ RB_STREAMLY_UNUSED void *data2)
56
+ {
57
+ VALUE rv;
58
+
59
+ TRAP_BEG;
60
+ rv = func(data1);
61
+ TRAP_END;
62
+
63
+ return rv;
64
+ }
65
+
66
+ #endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */