Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,23 @@
name: Testing on Ubuntu
on:
- push
- pull_request
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
ruby: [ '3.1', '3.2', '3.3', '3.4', 'head' ]
os: [ 'ubuntu-latest' ]
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: unit testing
run: |
gem install bundler rake
bundle install --jobs 4 --retry 3
bundle exec rake
@@ -0,0 +1,23 @@
name: Testing on Windows
on:
- push
- pull_request
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
ruby: [ '3.1', '3.2', '3.3', '3.4', 'head' ]
os: [ 'windows-latest' ]
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: unit testing
run: |
gem install bundler rake
bundle install --jobs 4 --retry 3
bundle exec rake
+12
View File
@@ -0,0 +1,12 @@
*.bundle
*.bundle
*.gem
*.jar
*.o
*.rbc
*.so
*.swp
Gemfile.lock
Makefile
tags
tmp
@@ -0,0 +1,6 @@
[submodule "http-parser"]
path = ext/ruby_http_parser/vendor/http-parser
url = https://github.com/nodejs/http-parser.git
[submodule "http-parser-java"]
path = ext/ruby_http_parser/vendor/http-parser-java
url = https://github.com/tmm1/http-parser.java
+2
View File
@@ -0,0 +1,2 @@
source 'https://rubygems.org'
gemspec
+20
View File
@@ -0,0 +1,20 @@
Copyright 2009,2010 Marc-André Cournoyer <macournoyer@gmail.com>
Copyright 2010,2011 Aman Gupta <aman@tmm1.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
+90
View File
@@ -0,0 +1,90 @@
# http_parser.rb
A simple callback-based HTTP request/response parser for writing http
servers, clients and proxies.
This gem is built on top of [joyent/http-parser](https://github.com/joyent/http-parser) and its java port [http-parser/http-parser.java](https://github.com/http-parser/http-parser.java).
## Supported Platforms
This gem aims to work on all major Ruby platforms, including:
- MRI 1.8, 1.9 and 2.0; should work on MRI 2.4+
- Rubinius
- JRuby
- win32
## Usage
```ruby
require "http/parser"
parser = Http::Parser.new
parser.on_headers_complete = proc do
p parser.http_version
p parser.http_method # for requests
p parser.request_url
p parser.status_code # for responses
p parser.headers
end
parser.on_body = proc do |chunk|
# One chunk of the body
p chunk
end
parser.on_message_complete = proc do |env|
# Headers and body is all parsed
puts "Done!"
end
```
# Feed raw data from the socket to the parser
`parser << raw_data`
## Advanced Usage
### Accept callbacks on an object
```ruby
module MyHttpConnection
def connection_completed
@parser = Http::Parser.new(self)
end
def receive_data(data)
@parser << data
end
def on_message_begin
@headers = nil
@body = ''
end
def on_headers_complete(headers)
@headers = headers
end
def on_body(chunk)
@body << chunk
end
def on_message_complete
p [@headers, @body]
end
end
```
### Stop parsing after headers
```ruby
parser = Http::Parser.new
parser.on_headers_complete = proc{ :stop }
offset = parser << request_data
body = request_data[offset..-1]
```
+8
View File
@@ -0,0 +1,8 @@
require 'bundler/gem_tasks'
# default task
task :compile => :submodules
task :default => [:compile, :spec]
# load tasks
Dir['tasks/*.rake'].sort.each { |f| load f }
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env ruby
$:.unshift File.dirname(__FILE__) + "/../lib"
require "rubygems"
require "http/parser"
require "benchmark/ips"
request = <<-REQUEST
GET / HTTP/1.1
Host: www.example.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.78 S
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
REQUEST
request.gsub!(/\n/m, "\r\n")
Benchmark.ips do |ips|
ips.report("instance") { Http::Parser.new }
ips.report("parsing") { Http::Parser.new << request }
end
@@ -0,0 +1,58 @@
$:.unshift File.dirname(__FILE__) + "/../lib"
require "rubygems"
require "thin_parser"
require "http_parser"
require "benchmark"
require "stringio"
data = "POST /postit HTTP/1.1\r\n" +
"Host: localhost:3000\r\n" +
"User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9\r\n" +
"Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\n" +
"Accept-Language: en-us,en;q=0.5\r\n" +
"Accept-Encoding: gzip,deflate\r\n" +
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
"Keep-Alive: 300\r\n" +
"Connection: keep-alive\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 37\r\n" +
"\r\n" +
"name=marc&email=macournoyer@gmail.com"
def thin(data)
env = {"rack.input" => StringIO.new}
Thin::HttpParser.new.execute(env, data, 0)
env
end
def http_parser(data)
body = StringIO.new
env = nil
parser = HTTP::RequestParser.new
parser.on_headers_complete = proc { |e| env = e }
parser.on_body = proc { |c| body << c }
parser << data
env["rack-input"] = body
env
end
# p thin(data)
# p http_parser(data)
TESTS = 30_000
Benchmark.bmbm do |results|
results.report("thin:") { TESTS.times { thin data } }
results.report("http-parser:") { TESTS.times { http_parser data } }
end
# On my MBP core duo 2.2Ghz
# Rehearsal ------------------------------------------------
# thin: 1.470000 0.000000 1.470000 ( 1.474737)
# http-parser: 1.270000 0.020000 1.290000 ( 1.292758)
# --------------------------------------- total: 2.760000sec
#
# user system total real
# thin: 1.150000 0.030000 1.180000 ( 1.173767)
# http-parser: 1.250000 0.010000 1.260000 ( 1.263796)
@@ -0,0 +1 @@
ryah_http_parser.*
@@ -0,0 +1,18 @@
import java.io.IOException;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.runtime.load.BasicLibraryService;
import org.ruby_http_parser.*;
public class RubyHttpParserService implements BasicLibraryService {
public boolean basicLoad(final Ruby runtime) throws IOException {
RubyModule mHTTP = runtime.defineModule("HTTP");
RubyClass cParser = mHTTP.defineClassUnder("Parser", runtime.getObject(), RubyHttpParser.ALLOCATOR);
cParser.defineAnnotatedMethods(RubyHttpParser.class);
cParser.defineClassUnder("Error", runtime.getClass("IOError"),runtime.getClass("IOError").getAllocator());
return true;
}
}
@@ -0,0 +1,23 @@
#ifndef ext_help_h
#define ext_help_h
#define RAISE_NOT_NULL(T) if(T == NULL) rb_raise(rb_eArgError, "NULL found for " # T " when shouldn't be.");
#ifdef TypedData_Get_Struct
#define DATA_GET_STRUCT(from,type,name) TypedData_Get_Struct(from,type,&type##_type,name)
#else
#define DATA_GET_STRUCT(from,type,name) Data_Get_Struct(from,type,name)
#endif
#define DATA_GET(from,type,name) DATA_GET_STRUCT(from,type,name); RAISE_NOT_NULL(name);
#define REQUIRE_TYPE(V, T) if(TYPE(V) != T) rb_raise(rb_eTypeError, "Wrong argument type for " # V " required " # T);
/* for compatibility with Ruby 1.8.5, which doesn't declare RSTRING_PTR */
#ifndef RSTRING_PTR
#define RSTRING_PTR(s) (RSTRING(s)->ptr)
#endif
/* for compatibility with Ruby 1.8.5, which doesn't declare RSTRING_LEN */
#ifndef RSTRING_LEN
#define RSTRING_LEN(s) (RSTRING(s)->len)
#endif
#endif
@@ -0,0 +1,24 @@
require 'mkmf'
# check out code if it hasn't been already
if Dir[File.expand_path('../vendor/http-parser/*', __FILE__)].empty?
Dir.chdir(File.expand_path('../../../', __FILE__)) do
xsystem 'git submodule init'
xsystem 'git submodule update'
end
end
# mongrel and http-parser both define http_parser_(init|execute), so we
# rename functions in http-parser before using them.
vendor_dir = File.expand_path('../vendor/http-parser/', __FILE__)
src_dir = File.expand_path('../', __FILE__)
%w[ http_parser.c http_parser.h ].each do |file|
File.open(File.join(src_dir, "ryah_#{file}"), 'w'){ |f|
f.write File.read(File.join(vendor_dir, file)).gsub('http_parser', 'ryah_http_parser')
}
end
$CFLAGS << " -I\"#{src_dir}\""
dir_config("ruby_http_parser")
create_makefile("ruby_http_parser")
@@ -0,0 +1,502 @@
package org.ruby_http_parser;
import http_parser.HTTPException;
import http_parser.HTTPMethod;
import http_parser.HTTPParser;
import http_parser.lolevel.HTTPCallback;
import http_parser.lolevel.HTTPDataCallback;
import http_parser.lolevel.ParserSettings;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import org.jcodings.Encoding;
import org.jcodings.specific.UTF8Encoding;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyHash;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.RubySymbol;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
public class RubyHttpParser extends RubyObject {
@JRubyMethod(name = "strict?", module = true)
public static IRubyObject strict(IRubyObject recv) {
return recv.getRuntime().newBoolean(true);
}
public static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new RubyHttpParser(runtime, klass);
}
};
byte[] fetchBytes(ByteBuffer b, int pos, int len) {
byte[] by = new byte[len];
int saved = b.position();
b.position(pos);
b.get(by);
b.position(saved);
return by;
}
public class StopException extends RuntimeException {
}
private Ruby runtime;
private HTTPParser parser;
private ParserSettings settings;
private RubyClass eParserError;
private RubyHash headers;
private IRubyObject on_message_begin;
private IRubyObject on_headers_complete;
private IRubyObject on_body;
private IRubyObject on_message_complete;
private IRubyObject requestUrl;
private IRubyObject requestPath;
private IRubyObject queryString;
private IRubyObject fragment;
private IRubyObject header_value_type;
private IRubyObject upgradeData;
private IRubyObject callback_object;
private boolean completed;
private byte[] _current_header;
private byte[] _last_header;
private static final Encoding UTF8 = UTF8Encoding.INSTANCE;
private static final List<String> VALUE_TYPES = new ArrayList<String>(
Arrays.asList("mixed", "arrays", "strings")
);
public RubyHttpParser(final Ruby runtime, RubyClass clazz) {
super(runtime, clazz);
this.runtime = runtime;
this.eParserError = (RubyClass) runtime.getModule("HTTP").getClass("Parser").getConstant("Error");
this.on_message_begin = null;
this.on_headers_complete = null;
this.on_body = null;
this.on_message_complete = null;
this.callback_object = null;
this.completed = false;
this.header_value_type = runtime.getModule("HTTP").getClass("Parser")
.getInstanceVariable("@default_header_value_type");
initSettings();
init();
}
private void initSettings() {
this.settings = new ParserSettings();
this.settings.on_url = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
byte[] data = fetchBytes(buf, pos, len);
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) requestUrl).cat(data, 0, data.length, UTF8);
} else {
((RubyString) requestUrl).cat(data);
}
return 0;
}
};
this.settings.on_header_field = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
byte[] data = fetchBytes(buf, pos, len);
if (_current_header == null)
_current_header = data;
else {
byte[] tmp = new byte[_current_header.length + data.length];
System.arraycopy(_current_header, 0, tmp, 0, _current_header.length);
System.arraycopy(data, 0, tmp, _current_header.length, data.length);
_current_header = tmp;
}
return 0;
}
};
final RubySymbol arraysSym = runtime.newSymbol("arrays");
final RubySymbol mixedSym = runtime.newSymbol("mixed");
final RubySymbol stopSym = runtime.newSymbol("stop");
final RubySymbol resetSym = runtime.newSymbol("reset");
this.settings.on_header_value = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
byte[] data = fetchBytes(buf, pos, len);
ThreadContext context = headers.getRuntime().getCurrentContext();
IRubyObject key, val;
int new_field = 0;
if (_current_header != null) {
new_field = 1;
_last_header = _current_header;
_current_header = null;
}
key = RubyString.newString(runtime, new ByteList(_last_header, UTF8, false));
val = headers.op_aref(context, key);
if (new_field == 1) {
if (val.isNil()) {
if (header_value_type == arraysSym) {
headers.op_aset(context, key,
RubyArray.newArrayLight(runtime, RubyString.newStringLight(runtime, 10, UTF8)));
} else {
headers.op_aset(context, key, RubyString.newStringLight(runtime, 10, UTF8));
}
} else {
if (header_value_type == mixedSym) {
if (val instanceof RubyString) {
headers.op_aset(context, key,
RubyArray.newArrayLight(runtime, val, RubyString.newStringLight(runtime, 10, UTF8)));
} else {
((RubyArray) val).add(RubyString.newStringLight(runtime, 10, UTF8));
}
} else if (header_value_type == arraysSym) {
((RubyArray) val).add(RubyString.newStringLight(runtime, 10, UTF8));
} else {
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) val).cat(',', UTF8).cat(' ', UTF8);
} else {
((RubyString) val).cat(',').cat(' ');
}
}
}
val = headers.op_aref(context, key);
}
if (val instanceof RubyArray) {
val = ((RubyArray) val).entry(-1);
}
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) val).cat(data, 0, data.length, UTF8);
} else {
((RubyString) val).cat(data);
}
return 0;
}
};
this.settings.on_message_begin = new HTTPCallback() {
public int cb(http_parser.lolevel.HTTPParser p) {
headers = new RubyHash(runtime);
if (runtime.is1_9() || runtime.is2_0()) {
requestUrl = RubyString.newEmptyString(runtime, UTF8);
requestPath = RubyString.newEmptyString(runtime, UTF8);
queryString = RubyString.newEmptyString(runtime, UTF8);
fragment = RubyString.newEmptyString(runtime, UTF8);
upgradeData = RubyString.newEmptyString(runtime, UTF8);
} else {
requestUrl = RubyString.newEmptyString(runtime);
requestPath = RubyString.newEmptyString(runtime);
queryString = RubyString.newEmptyString(runtime);
fragment = RubyString.newEmptyString(runtime);
upgradeData = RubyString.newEmptyString(runtime);
}
IRubyObject ret = runtime.getNil();
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_message_begin")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_message_begin");
}
} else if (on_message_begin != null) {
ThreadContext context = on_message_begin.getRuntime().getCurrentContext();
ret = on_message_begin.callMethod(context, "call");
}
if (ret == stopSym) {
throw new StopException();
} else {
return 0;
}
}
};
this.settings.on_message_complete = new HTTPCallback() {
public int cb(http_parser.lolevel.HTTPParser p) {
IRubyObject ret = runtime.getNil();
completed = true;
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_message_complete")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_message_complete");
}
} else if (on_message_complete != null) {
ThreadContext context = on_message_complete.getRuntime().getCurrentContext();
ret = on_message_complete.callMethod(context, "call");
}
if (ret == stopSym) {
throw new StopException();
} else {
return 0;
}
}
};
this.settings.on_headers_complete = new HTTPCallback() {
public int cb(http_parser.lolevel.HTTPParser p) {
IRubyObject ret = runtime.getNil();
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_headers_complete")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_headers_complete", headers);
}
} else if (on_headers_complete != null) {
ThreadContext context = on_headers_complete.getRuntime().getCurrentContext();
ret = on_headers_complete.callMethod(context, "call", headers);
}
if (ret == stopSym) {
throw new StopException();
} else if (ret == resetSym) {
return 1;
} else {
return 0;
}
}
};
this.settings.on_body = new HTTPDataCallback() {
public int cb(http_parser.lolevel.HTTPParser p, ByteBuffer buf, int pos, int len) {
IRubyObject ret = runtime.getNil();
byte[] data = fetchBytes(buf, pos, len);
if (callback_object != null) {
if (((RubyObject) callback_object).respondsTo("on_body")) {
ThreadContext context = callback_object.getRuntime().getCurrentContext();
ret = callback_object.callMethod(context, "on_body",
RubyString.newString(runtime, new ByteList(data, UTF8, false)));
}
} else if (on_body != null) {
ThreadContext context = on_body.getRuntime().getCurrentContext();
ret = on_body.callMethod(context, "call", RubyString.newString(runtime, new ByteList(data, UTF8, false)));
}
if (ret == stopSym) {
throw new StopException();
} else {
return 0;
}
}
};
}
private void init() {
this.parser = new HTTPParser();
this.parser.HTTP_PARSER_STRICT = true;
this.headers = null;
this.requestUrl = runtime.getNil();
this.requestPath = runtime.getNil();
this.queryString = runtime.getNil();
this.fragment = runtime.getNil();
this.upgradeData = runtime.getNil();
}
@JRubyMethod(name = "initialize")
public IRubyObject initialize() {
return this;
}
@JRubyMethod(name = "initialize")
public IRubyObject initialize(IRubyObject arg) {
callback_object = arg;
return initialize();
}
@JRubyMethod(name = "initialize")
public IRubyObject initialize(IRubyObject arg, IRubyObject arg2) {
header_value_type = arg2;
return initialize(arg);
}
@JRubyMethod(name = "on_message_begin=")
public IRubyObject set_on_message_begin(IRubyObject cb) {
on_message_begin = cb;
return cb;
}
@JRubyMethod(name = "on_headers_complete=")
public IRubyObject set_on_headers_complete(IRubyObject cb) {
on_headers_complete = cb;
return cb;
}
@JRubyMethod(name = "on_body=")
public IRubyObject set_on_body(IRubyObject cb) {
on_body = cb;
return cb;
}
@JRubyMethod(name = "on_message_complete=")
public IRubyObject set_on_message_complete(IRubyObject cb) {
on_message_complete = cb;
return cb;
}
@JRubyMethod(name = "<<")
public IRubyObject execute(IRubyObject data) {
RubyString str = (RubyString) data;
ByteList byteList = str.getByteList();
ByteBuffer buf = ByteBuffer.wrap(byteList.getUnsafeBytes(), byteList.getBegin(), byteList.getRealSize());
boolean stopped = false;
try {
this.parser.execute(this.settings, buf);
} catch (HTTPException e) {
throw new RaiseException(runtime, eParserError, e.getMessage(), true);
} catch (StopException e) {
stopped = true;
}
if (parser.getUpgrade()) {
byte[] upData = fetchBytes(buf, buf.position(), buf.limit() - buf.position());
if (runtime.is1_9() || runtime.is2_0()) {
((RubyString) upgradeData).cat(upData, 0, upData.length, UTF8);
} else {
((RubyString) upgradeData).cat(upData);
}
} else if (buf.hasRemaining() && !completed) {
if (!stopped)
throw new RaiseException(runtime, eParserError, "Could not parse data entirely", true);
}
return RubyNumeric.int2fix(runtime, buf.position());
}
@JRubyMethod(name = "keep_alive?")
public IRubyObject shouldKeepAlive() {
return runtime.newBoolean(parser.shouldKeepAlive());
}
@JRubyMethod(name = "upgrade?")
public IRubyObject shouldUpgrade() {
return runtime.newBoolean(parser.getUpgrade());
}
@JRubyMethod(name = "http_major")
public IRubyObject httpMajor() {
if (parser.getMajor() == 0 && parser.getMinor() == 0)
return runtime.getNil();
else
return RubyNumeric.int2fix(runtime, parser.getMajor());
}
@JRubyMethod(name = "http_minor")
public IRubyObject httpMinor() {
if (parser.getMajor() == 0 && parser.getMinor() == 0)
return runtime.getNil();
else
return RubyNumeric.int2fix(runtime, parser.getMinor());
}
@JRubyMethod(name = "http_version")
public IRubyObject httpVersion() {
if (parser.getMajor() == 0 && parser.getMinor() == 0)
return runtime.getNil();
else
return runtime.newArray(httpMajor(), httpMinor());
}
@JRubyMethod(name = "http_method")
public IRubyObject httpMethod() {
HTTPMethod method = parser.getHTTPMethod();
if (method != null)
return runtime.newString(new String(method.bytes));
else
return runtime.getNil();
}
@JRubyMethod(name = "status_code")
public IRubyObject statusCode() {
int code = parser.getStatusCode();
if (code != 0)
return RubyNumeric.int2fix(runtime, code);
else
return runtime.getNil();
}
@JRubyMethod(name = "headers")
public IRubyObject getHeaders() {
return headers == null ? runtime.getNil() : headers;
}
@JRubyMethod(name = "request_url")
public IRubyObject getRequestUrl() {
return requestUrl == null ? runtime.getNil() : requestUrl;
}
@JRubyMethod(name = "request_path")
public IRubyObject getRequestPath() {
return requestPath == null ? runtime.getNil() : requestPath;
}
@JRubyMethod(name = "query_string")
public IRubyObject getQueryString() {
return queryString == null ? runtime.getNil() : queryString;
}
@JRubyMethod(name = "fragment")
public IRubyObject getFragment() {
return fragment == null ? runtime.getNil() : fragment;
}
@JRubyMethod(name = "header_value_type")
public IRubyObject getHeaderValueType() {
return header_value_type == null ? runtime.getNil() : header_value_type;
}
@JRubyMethod(name = "header_value_type=")
public IRubyObject set_header_value_type(IRubyObject val) {
String valString = val.toString();
if (!VALUE_TYPES.contains(valString)) {
throw runtime.newArgumentError("Invalid header value type");
}
header_value_type = val;
return val;
}
@JRubyMethod(name = "upgrade_data")
public IRubyObject upgradeData() {
return upgradeData == null ? runtime.getNil() : upgradeData;
}
@JRubyMethod(name = "reset!")
public IRubyObject reset() {
init();
return runtime.getTrue();
}
}
@@ -0,0 +1,569 @@
#include "ruby.h"
#include "ext_help.h"
#include "ryah_http_parser.h"
#define GET_WRAPPER(N, from) ParserWrapper *N = (ParserWrapper *)(from)->data;
#define HASH_CAT(h, k, ptr, len) \
do { \
VALUE __v = rb_hash_aref(h, k); \
if (__v != Qnil) { \
rb_str_cat(__v, ptr, len); \
} else { \
rb_hash_aset(h, k, rb_str_new(ptr, len)); \
} \
} while(0)
typedef struct ParserWrapper {
ryah_http_parser parser;
VALUE status;
VALUE request_url;
VALUE headers;
VALUE upgrade_data;
VALUE on_message_begin;
VALUE on_headers_complete;
VALUE on_body;
VALUE on_message_complete;
VALUE callback_object;
VALUE stopped;
VALUE completed;
VALUE header_value_type;
VALUE last_field_name;
VALUE curr_field_name;
enum ryah_http_parser_type type;
} ParserWrapper;
void ParserWrapper_init(ParserWrapper *wrapper) {
ryah_http_parser_init(&wrapper->parser, wrapper->type);
wrapper->parser.status_code = 0;
wrapper->parser.http_major = 0;
wrapper->parser.http_minor = 0;
wrapper->status = Qnil;
wrapper->request_url = Qnil;
wrapper->upgrade_data = Qnil;
wrapper->headers = Qnil;
wrapper->completed = Qfalse;
wrapper->last_field_name = Qnil;
wrapper->curr_field_name = Qnil;
}
void ParserWrapper_mark(void *data) {
if(data) {
ParserWrapper *wrapper = (ParserWrapper *) data;
rb_gc_mark_maybe(wrapper->status);
rb_gc_mark_maybe(wrapper->request_url);
rb_gc_mark_maybe(wrapper->upgrade_data);
rb_gc_mark_maybe(wrapper->headers);
rb_gc_mark_maybe(wrapper->on_message_begin);
rb_gc_mark_maybe(wrapper->on_headers_complete);
rb_gc_mark_maybe(wrapper->on_body);
rb_gc_mark_maybe(wrapper->on_message_complete);
rb_gc_mark_maybe(wrapper->callback_object);
rb_gc_mark_maybe(wrapper->last_field_name);
rb_gc_mark_maybe(wrapper->curr_field_name);
}
}
void ParserWrapper_free(void *data) {
if(data) {
free(data);
}
}
static const rb_data_type_t ParserWrapper_type = {
"ParserWrapper",
{
ParserWrapper_mark,
ParserWrapper_free,
},
};
static VALUE cParser;
static VALUE cRequestParser;
static VALUE cResponseParser;
static VALUE eParserError;
static ID Icall;
static ID Ion_message_begin;
static ID Ion_headers_complete;
static ID Ion_body;
static ID Ion_message_complete;
static VALUE Sstop;
static VALUE Sreset;
static VALUE Sarrays;
static VALUE Sstrings;
static VALUE Smixed;
/** Callbacks **/
int on_message_begin(ryah_http_parser *parser) {
GET_WRAPPER(wrapper, parser);
wrapper->status = rb_str_new2("");
wrapper->request_url = rb_str_new2("");
wrapper->headers = rb_hash_new();
wrapper->upgrade_data = rb_str_new2("");
VALUE ret = Qnil;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_message_begin)) {
ret = rb_funcall(wrapper->callback_object, Ion_message_begin, 0);
} else if (wrapper->on_message_begin != Qnil) {
ret = rb_funcall(wrapper->on_message_begin, Icall, 0);
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else {
return 0;
}
}
int on_status(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
if (at && length) {
if (wrapper->status == Qnil) {
wrapper->status = rb_str_new(at, length);
} else {
rb_str_cat(wrapper->status, at, length);
}
}
return 0;
}
int on_url(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
if (at && length) {
if (wrapper->request_url == Qnil) {
wrapper->request_url = rb_str_new(at, length);
} else {
rb_str_cat(wrapper->request_url, at, length);
}
}
return 0;
}
int on_header_field(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
if (wrapper->curr_field_name == Qnil) {
wrapper->last_field_name = Qnil;
wrapper->curr_field_name = rb_str_new(at, length);
} else {
rb_str_cat(wrapper->curr_field_name, at, length);
}
return 0;
}
int on_header_value(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
int new_field = 0;
VALUE current_value;
if (wrapper->last_field_name == Qnil) {
new_field = 1;
wrapper->last_field_name = wrapper->curr_field_name;
wrapper->curr_field_name = Qnil;
}
current_value = rb_hash_aref(wrapper->headers, wrapper->last_field_name);
if (new_field == 1) {
if (current_value == Qnil) {
if (wrapper->header_value_type == Sarrays) {
rb_hash_aset(wrapper->headers, wrapper->last_field_name, rb_ary_new3(1, rb_str_new2("")));
} else {
rb_hash_aset(wrapper->headers, wrapper->last_field_name, rb_str_new2(""));
}
} else {
if (wrapper->header_value_type == Smixed) {
if (TYPE(current_value) == T_STRING) {
rb_hash_aset(wrapper->headers, wrapper->last_field_name, rb_ary_new3(2, current_value, rb_str_new2("")));
} else {
rb_ary_push(current_value, rb_str_new2(""));
}
} else if (wrapper->header_value_type == Sarrays) {
rb_ary_push(current_value, rb_str_new2(""));
} else {
rb_str_cat(current_value, ", ", 2);
}
}
current_value = rb_hash_aref(wrapper->headers, wrapper->last_field_name);
}
if (TYPE(current_value) == T_ARRAY) {
current_value = rb_ary_entry(current_value, -1);
}
rb_str_cat(current_value, at, length);
return 0;
}
int on_headers_complete(ryah_http_parser *parser) {
GET_WRAPPER(wrapper, parser);
VALUE ret = Qnil;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_headers_complete)) {
ret = rb_funcall(wrapper->callback_object, Ion_headers_complete, 1, wrapper->headers);
} else if (wrapper->on_headers_complete != Qnil) {
ret = rb_funcall(wrapper->on_headers_complete, Icall, 1, wrapper->headers);
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else if (ret == Sreset){
return 1;
} else {
return 0;
}
}
int on_body(ryah_http_parser *parser, const char *at, size_t length) {
GET_WRAPPER(wrapper, parser);
VALUE ret = Qnil;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_body)) {
ret = rb_funcall(wrapper->callback_object, Ion_body, 1, rb_str_new(at, length));
} else if (wrapper->on_body != Qnil) {
ret = rb_funcall(wrapper->on_body, Icall, 1, rb_str_new(at, length));
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else {
return 0;
}
}
int on_message_complete(ryah_http_parser *parser) {
GET_WRAPPER(wrapper, parser);
VALUE ret = Qnil;
wrapper->completed = Qtrue;
if (wrapper->callback_object != Qnil && rb_respond_to(wrapper->callback_object, Ion_message_complete)) {
ret = rb_funcall(wrapper->callback_object, Ion_message_complete, 0);
} else if (wrapper->on_message_complete != Qnil) {
ret = rb_funcall(wrapper->on_message_complete, Icall, 0);
}
if (ret == Sstop) {
wrapper->stopped = Qtrue;
return -1;
} else {
return 0;
}
}
static ryah_http_parser_settings settings = {
.on_message_begin = on_message_begin,
.on_status = on_status,
.on_url = on_url,
.on_header_field = on_header_field,
.on_header_value = on_header_value,
.on_headers_complete = on_headers_complete,
.on_body = on_body,
.on_message_complete = on_message_complete
};
VALUE Parser_alloc_by_type(VALUE klass, enum ryah_http_parser_type type) {
ParserWrapper *wrapper = ALLOC_N(ParserWrapper, 1);
wrapper->type = type;
wrapper->parser.data = wrapper;
wrapper->on_message_begin = Qnil;
wrapper->on_headers_complete = Qnil;
wrapper->on_body = Qnil;
wrapper->on_message_complete = Qnil;
wrapper->callback_object = Qnil;
ParserWrapper_init(wrapper);
return TypedData_Wrap_Struct(klass, &ParserWrapper_type, wrapper);
}
VALUE Parser_alloc(VALUE klass) {
return Parser_alloc_by_type(klass, HTTP_BOTH);
}
VALUE RequestParser_alloc(VALUE klass) {
return Parser_alloc_by_type(klass, HTTP_REQUEST);
}
VALUE ResponseParser_alloc(VALUE klass) {
return Parser_alloc_by_type(klass, HTTP_RESPONSE);
}
VALUE Parser_strict_p(VALUE klass) {
return HTTP_PARSER_STRICT == 1 ? Qtrue : Qfalse;
}
VALUE Parser_initialize(int argc, VALUE *argv, VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
VALUE default_header_value_type = Qnil;
if (argc > 0 && RB_TYPE_P(argv[argc-1], T_HASH)) {
ID keyword_ids[1];
keyword_ids[0] = rb_intern("default_header_value_type");
rb_get_kwargs(argv[argc-1], keyword_ids, 0, 1, &default_header_value_type);
if (default_header_value_type == Qundef) {
default_header_value_type = Qnil;
}
--argc;
}
if (argc == 1) {
wrapper->callback_object = argv[0];
}
if (argc == 2) {
wrapper->callback_object = argv[0];
default_header_value_type = argv[1];
}
if (default_header_value_type == Qnil) {
wrapper->header_value_type = rb_iv_get(CLASS_OF(self), "@default_header_value_type");
} else {
wrapper->header_value_type = default_header_value_type;
}
return self;
}
VALUE Parser_execute(VALUE self, VALUE data) {
ParserWrapper *wrapper = NULL;
Check_Type(data, T_STRING);
char *ptr = RSTRING_PTR(data);
long len = RSTRING_LEN(data);
DATA_GET(self, ParserWrapper, wrapper);
wrapper->stopped = Qfalse;
size_t nparsed = ryah_http_parser_execute(&wrapper->parser, &settings, ptr, len);
if (wrapper->parser.upgrade) {
if (RTEST(wrapper->stopped) && !RTEST(wrapper->completed))
nparsed += 1;
if (nparsed < len)
rb_str_cat(wrapper->upgrade_data, ptr + nparsed, len - nparsed);
} else if (nparsed != (size_t)len) {
if (!RTEST(wrapper->stopped) && !RTEST(wrapper->completed))
rb_raise(eParserError, "Could not parse data entirely (%zu != %zu)", nparsed, len);
else
nparsed += 1; // error states fail on the current character
}
return INT2FIX(nparsed);
}
VALUE Parser_set_on_message_begin(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_message_begin = callback;
return callback;
}
VALUE Parser_set_on_headers_complete(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_headers_complete = callback;
return callback;
}
VALUE Parser_set_on_body(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_body = callback;
return callback;
}
VALUE Parser_set_on_message_complete(VALUE self, VALUE callback) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->on_message_complete = callback;
return callback;
}
VALUE Parser_keep_alive_p(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
return http_should_keep_alive(&wrapper->parser) == 1 ? Qtrue : Qfalse;
}
VALUE Parser_upgrade_p(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
return wrapper->parser.upgrade ? Qtrue : Qfalse;
}
VALUE Parser_http_version(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.http_major == 0 && wrapper->parser.http_minor == 0)
return Qnil;
else
return rb_ary_new3(2, INT2FIX(wrapper->parser.http_major), INT2FIX(wrapper->parser.http_minor));
}
VALUE Parser_http_major(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.http_major == 0 && wrapper->parser.http_minor == 0)
return Qnil;
else
return INT2FIX(wrapper->parser.http_major);
}
VALUE Parser_http_minor(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.http_major == 0 && wrapper->parser.http_minor == 0)
return Qnil;
else
return INT2FIX(wrapper->parser.http_minor);
}
VALUE Parser_http_method(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.type == HTTP_REQUEST)
return rb_str_new2(http_method_str(wrapper->parser.method));
else
return Qnil;
}
VALUE Parser_status_code(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
if (wrapper->parser.status_code)
return INT2FIX(wrapper->parser.status_code);
else
return Qnil;
}
#define DEFINE_GETTER(name) \
VALUE Parser_##name(VALUE self) { \
ParserWrapper *wrapper = NULL; \
DATA_GET(self, ParserWrapper, wrapper); \
return wrapper->name; \
}
DEFINE_GETTER(status);
DEFINE_GETTER(request_url);
DEFINE_GETTER(headers);
DEFINE_GETTER(upgrade_data);
DEFINE_GETTER(header_value_type);
VALUE Parser_set_header_value_type(VALUE self, VALUE val) {
if (val != Sarrays && val != Sstrings && val != Smixed) {
rb_raise(rb_eArgError, "Invalid header value type");
}
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
wrapper->header_value_type = val;
return wrapper->header_value_type;
}
VALUE Parser_reset(VALUE self) {
ParserWrapper *wrapper = NULL;
DATA_GET(self, ParserWrapper, wrapper);
ParserWrapper_init(wrapper);
return Qtrue;
}
void Init_ruby_http_parser() {
#ifdef HAVE_RB_EXT_RACTOR_SAFE
rb_ext_ractor_safe(true);
#endif
VALUE mHTTP = rb_define_module("HTTP");
cParser = rb_define_class_under(mHTTP, "Parser", rb_cObject);
cRequestParser = rb_define_class_under(mHTTP, "RequestParser", cParser);
cResponseParser = rb_define_class_under(mHTTP, "ResponseParser", cParser);
eParserError = rb_define_class_under(cParser, "Error", rb_eIOError);
Icall = rb_intern("call");
Ion_message_begin = rb_intern("on_message_begin");
Ion_headers_complete = rb_intern("on_headers_complete");
Ion_body = rb_intern("on_body");
Ion_message_complete = rb_intern("on_message_complete");
Sstop = ID2SYM(rb_intern("stop"));
Sreset = ID2SYM(rb_intern("reset"));
Sarrays = ID2SYM(rb_intern("arrays"));
Sstrings = ID2SYM(rb_intern("strings"));
Smixed = ID2SYM(rb_intern("mixed"));
rb_define_alloc_func(cParser, Parser_alloc);
rb_define_alloc_func(cRequestParser, RequestParser_alloc);
rb_define_alloc_func(cResponseParser, ResponseParser_alloc);
rb_define_singleton_method(cParser, "strict?", Parser_strict_p, 0);
rb_define_method(cParser, "initialize", Parser_initialize, -1);
rb_define_method(cParser, "on_message_begin=", Parser_set_on_message_begin, 1);
rb_define_method(cParser, "on_headers_complete=", Parser_set_on_headers_complete, 1);
rb_define_method(cParser, "on_body=", Parser_set_on_body, 1);
rb_define_method(cParser, "on_message_complete=", Parser_set_on_message_complete, 1);
rb_define_method(cParser, "<<", Parser_execute, 1);
rb_define_method(cParser, "keep_alive?", Parser_keep_alive_p, 0);
rb_define_method(cParser, "upgrade?", Parser_upgrade_p, 0);
rb_define_method(cParser, "http_version", Parser_http_version, 0);
rb_define_method(cParser, "http_major", Parser_http_major, 0);
rb_define_method(cParser, "http_minor", Parser_http_minor, 0);
rb_define_method(cParser, "http_method", Parser_http_method, 0);
rb_define_method(cParser, "status_code", Parser_status_code, 0);
rb_define_method(cParser, "status", Parser_status, 0);
rb_define_method(cParser, "request_url", Parser_request_url, 0);
rb_define_method(cParser, "headers", Parser_headers, 0);
rb_define_method(cParser, "upgrade_data", Parser_upgrade_data, 0);
rb_define_method(cParser, "header_value_type", Parser_header_value_type, 0);
rb_define_method(cParser, "header_value_type=", Parser_set_header_value_type, 1);
rb_define_method(cParser, "reset!", Parser_reset, 0);
}
@@ -0,0 +1,32 @@
Gem::Specification.new do |s|
s.name = "http_parser.rb"
s.version = "0.8.1"
s.summary = "Simple callback-based HTTP request/response parser"
s.description = "Ruby bindings to https://github.com/joyent/http-parser and https://github.com/http-parser/http-parser.java"
s.authors = ["Marc-Andre Cournoyer", "Aman Gupta"]
s.email = ["macournoyer@gmail.com", "aman@tmm1.net"]
s.license = 'MIT'
s.homepage = "https://github.com/tmm1/http_parser.rb"
s.files = `git ls-files`.split("\n").grep_v(%r{spec/}) + Dir['ext/ruby_http_parser/vendor/**/*']
s.require_paths = ["lib"]
s.extensions = ["ext/ruby_http_parser/extconf.rb"]
s.add_development_dependency 'rake-compiler', '~> 1.0'
s.add_development_dependency 'rspec', '~> 3'
s.add_development_dependency 'json', '~> 2.1'
s.add_development_dependency 'benchmark_suite', '~> 1.0'
s.add_development_dependency 'ffi', '~> 1.9'
if RUBY_PLATFORM =~ /java/
s.add_development_dependency 'jruby-openssl'
else
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4.0')
s.add_development_dependency 'yajl-ruby', '~> 1.3'
else
s.add_development_dependency 'yajl-ruby', '= 1.2.1'
end
end
end
@@ -0,0 +1 @@
require 'http_parser'
@@ -0,0 +1,21 @@
$:.unshift File.expand_path('../', __FILE__)
require 'ruby_http_parser'
Http = HTTP
module HTTP
class Parser
class << self
attr_reader :default_header_value_type
def default_header_value_type=(val)
if (val != :mixed && val != :strings && val != :arrays)
raise ArgumentError, "Invalid header value type"
end
@default_header_value_type = val
end
end
end
end
HTTP::Parser.default_header_value_type = :mixed
@@ -0,0 +1,42 @@
require 'rubygems/package_task'
require 'rake/extensiontask'
require 'rake/javaextensiontask'
def gemspec
@clean_gemspec ||= eval(File.read(File.expand_path('../../http_parser.rb.gemspec', __FILE__)))
end
Gem::PackageTask.new(gemspec) do |pkg|
end
if RUBY_PLATFORM =~ /java/
Rake::JavaExtensionTask.new("ruby_http_parser", gemspec) do |ext|
ext.classpath = File.expand_path('../../ext/ruby_http_parser/vendor/http-parser-java/ext/primitives.jar', __FILE__)
end
else
Rake::ExtensionTask.new("ruby_http_parser", gemspec) do |ext|
unless RUBY_PLATFORM =~ /mswin|mingw/
ext.cross_compile = true
ext.cross_platform = ['x86-mingw32', 'x86-mswin32-60']
# inject 1.8/1.9 pure-ruby entry point
ext.cross_compiling do |spec|
spec.files += ['lib/ruby_http_parser.rb']
end
end
end
end
file 'lib/ruby_http_parser.rb' do |t|
File.open(t.name, 'wb') do |f|
f.write <<-eoruby
RUBY_VERSION =~ /(\\d+.\\d+)/
require "\#{$1}/ruby_http_parser"
eoruby
end
at_exit{ FileUtils.rm t.name if File.exist?(t.name) }
end
if Rake::Task.task_defined?(:cross)
task :cross => 'lib/ruby_http_parser.rb'
end
@@ -0,0 +1,71 @@
desc "Generate test fixtures"
task :fixtures => :submodules do
require 'yajl'
data = File.read File.expand_path('../../ext/ruby_http_parser/vendor/http-parser/test.c', __FILE__)
%w[ requests responses ].each do |type|
# find test definitions in between requests/responses[]= and .name=NULL
tmp = data[/#{type}\[\]\s*=(.+?),\s*\{\s*\.name=\s*NULL/m, 1]
# replace first { with a [ (parsing an array of test cases)
tmp.sub!('{','[')
# replace booleans
tmp.gsub!('TRUE', 'true')
tmp.gsub!('FALSE', 'false')
# mark strict mode tests
tmp.gsub!(%r|#if\s+!HTTP_PARSER_STRICT(.+?)#endif\s*/\*\s*!HTTP_PARSER_STRICT.+\n|m){
$1.gsub(/^(.+,\.type= .+)$/, "\\1\n, .strict= false")
}
# remove macros and comments
tmp.gsub!(/^#(if|elif|endif|define).+$/,'')
tmp.gsub!(/\/\*(.+?)\*\/$/,'')
# HTTP_* enums become strings
tmp.gsub!(/(= )(HTTP_\w+)/){
"#{$1}#{$2.sub('MSEARCH','M-SEARCH').dump}"
}
# join multiline strings for body and raw data
tmp.gsub!(/((body|raw)\s*=)(.+?)(\n\s+[\},])/m){
before, after = $1, $4
raw = $3.split("\n").map{ |l| l.strip[1..-2] }.join('')
"#{before} \"#{raw}\" #{after}"
}
# make headers an array of array tuples
tmp.gsub!(/(\.headers\s*=)(.+?)(\s*,\.)/m){
before, after = $1, $3
raw = $2.gsub('{', '[').gsub('}', ']')
"#{before} #{raw} #{after}"
}
# .name= becomes "name":
tmp.gsub!(/^(.{2,5})\.(\w+)\s*=/){
"#{$1}#{$2.dump}: "
}
# evaluate addition expressions
tmp.gsub!(/(body_size\":\s*)(\d+)\+(\d+)/){
"#{$1}#{$2.to_i+$3.to_i}"
}
# end result array
tmp << ']'
# normalize data
results = Yajl.load(tmp, :symbolize_keys => true)
results.map{ |res|
res[:headers] and res[:headers] = Hash[*res[:headers].flatten]
res[:method] and res[:method].gsub!(/^HTTP_/, '')
res[:strict] = true unless res.has_key?(:strict)
}
# write to a file
File.open("spec/support/#{type}.json", 'w'){ |f|
f.write Yajl.dump(results, :pretty => true)
}
end
end
@@ -0,0 +1,5 @@
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new do |t|
t.rspec_opts = %w(--format documentation --color)
end
@@ -0,0 +1,7 @@
desc "Fetch upstream submodules"
task :submodules do
if Dir['ext/ruby_http_parser/vendor/http-parser/*'].empty?
sh 'git submodule init'
sh 'git submodule update'
end
end