api-testing 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree@contracted
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in api-testing.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,32 @@
1
+ Contracted
2
+ ==========================================================
3
+ Contracted provides a few tools to describe HTTP Responses
4
+ and HTTP Response bodies.
5
+
6
+ A few cucumber steps are implemented which faciliated this
7
+ description.
8
+
9
+
10
+ JSON Description Grammar
11
+ ----------------------------------------------------------
12
+ The JSON Description Grammar is a superset of JSON which
13
+ adds some wildcards allowing you to describe the parts of
14
+ the response your interested in. Example:
15
+
16
+ {
17
+ "message": {
18
+ "text": ...,
19
+ ...
20
+ }
21
+ }
22
+
23
+ This says that the 'message' object has an entry with key
24
+ 'text' and any number of other entries.
25
+
26
+ The cucumber features provide more examples.
27
+
28
+ The grammar is based on the JSON parser from the following
29
+ blog post:
30
+
31
+ http://coderrr.wordpress.com/2008/02/13/treetop-is-cool/
32
+
@@ -0,0 +1,18 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../../lib')
2
+
3
+ require 'bundler'
4
+ require 'cucumber/rake/task'
5
+ require 'rspec/core/rake_task'
6
+
7
+ desc 'Runs specs'
8
+ RSpec::Core::RakeTask.new do |t|
9
+ end
10
+
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ desc 'Runs features'
14
+ Cucumber::Rake::Task.new do |t|
15
+ t.cucumber_opts = %w{--format pretty}
16
+ end
17
+
18
+ task :default => [:spec, :cucumber]
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "contracted/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "api-testing"
7
+ s.version = Contracted::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Andrew Kiellor"]
10
+ s.email = ["akiellor@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Contracted is a tool aimed at testing JSON API's.}
13
+ s.description = %q{
14
+ Contracted is a tool aimed at testing JSON API's.
15
+ }
16
+
17
+ s.rubyforge_project = "contracted"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ s.add_dependency('cucumber')
24
+ s.add_dependency('thin')
25
+ s.add_dependency('rest-client')
26
+ s.add_dependency('activesupport')
27
+ s.add_dependency('rspec')
28
+ s.add_dependency('treetop')
29
+ s.add_dependency('polyglot')
30
+ end
@@ -0,0 +1,18 @@
1
+ Feature: Echo app responds with message
2
+
3
+ Scenario: GET with message 'hello'
4
+ Given the "Echo" app is running
5
+ When an api client performs GET /?message=hello
6
+ Then the json response body should look like:
7
+ """
8
+ {"message": "hello"}
9
+ """
10
+
11
+ Scenario: GET with any message
12
+ Given the "Echo" app is running
13
+ When an api client performs GET /?message=blah
14
+ Then the json response body should look like:
15
+ """
16
+ {"message": ...}
17
+ """
18
+
@@ -0,0 +1,71 @@
1
+ Feature: EchoBody app responds with message
2
+
3
+ Scenario: POST with body '{"message": "hello"}'
4
+ Given the "EchoBody" app is running
5
+ When an api client performs POST / with json body:
6
+ """
7
+ {'message': 'hello'}
8
+ """
9
+ Then the json response body should look like:
10
+ """
11
+ {"message": "hello"}
12
+ """
13
+
14
+ Scenario: POST with body '{"message": "hello", "sender": "Tom"}'
15
+ Given the "EchoBody" app is running
16
+ When an api client performs POST / with json body:
17
+ """
18
+ {
19
+ "message": "hello",
20
+ "sender": "Tom"
21
+ }
22
+ """
23
+ Then the json response body should look like:
24
+ """
25
+ {
26
+ "message": "hello",
27
+ "sender": ...
28
+ }
29
+ """
30
+
31
+ Scenario: POST with nested object body
32
+ Given the "EchoBody" app is running
33
+ When an api client performs POST / with json body:
34
+ """
35
+ {
36
+ "message": {
37
+ "sender": "Tom",
38
+ "text": "Hello"
39
+ }
40
+ }
41
+ """
42
+ Then the json response body should look like:
43
+ """
44
+ {
45
+ "message": {
46
+ "sender": "Tom",
47
+ "text": ...
48
+ }
49
+ }
50
+ """
51
+
52
+ Scenario: POST with nested object body using wilcard match
53
+ Given the "EchoBody" app is running
54
+ When an api client performs POST / with json body:
55
+ """
56
+ {
57
+ "message": {
58
+ "sender": "Tom",
59
+ "text": "Hello"
60
+ }
61
+ }
62
+ """
63
+ Then the json response body should look like:
64
+ """
65
+ {
66
+ "message": {
67
+ "sender": ...,
68
+ ...
69
+ }
70
+ }
71
+ """
@@ -0,0 +1,17 @@
1
+ Feature: HelloWorld app says "Hello World"
2
+
3
+ Scenario: GET /
4
+ Given the "HelloWorld" app is running
5
+ When an api client performs GET /
6
+ Then the json response body should look like:
7
+ """
8
+ {"message": "Hello World"}
9
+ """
10
+
11
+ Scenario: GET / has message
12
+ Given the "HelloWorld" app is running
13
+ When an api client performs GET /
14
+ Then the json response body should look like:
15
+ """
16
+ {"message": ...}
17
+ """
@@ -0,0 +1,6 @@
1
+ class Echo
2
+ def call env
3
+ message = Rack::Request.new(env).params["message"]
4
+ [200, {'Content-Type' => 'application/json'}, "{'message': '#{message}'}"]
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class EchoBody
2
+ def call env
3
+ [200, {'Content-Type' => 'application/json'}, Rack::Request.new(env).body]
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class HelloWorld
2
+ def call env
3
+ [200, {'Content-Type' => 'application/json'}, '{"message": "Hello World"}']
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
2
+ require 'contracted'
3
+ require 'contracted/cucumber'
@@ -0,0 +1,5 @@
1
+ require 'polyglot'
2
+ require 'treetop'
3
+
4
+ require 'contracted/application'
5
+ require 'contracted/json_description'
@@ -0,0 +1,61 @@
1
+ require 'thin'
2
+ require 'rest_client'
3
+ Thin::Logging.silent = true
4
+
5
+ module Contracted
6
+ def self.mount app, port = '9898'
7
+ @app = Application.new app, port.to_s
8
+ end
9
+
10
+ def self.app
11
+ @app
12
+ end
13
+
14
+ class Application
15
+ APPLICATION_START_TIMEOUT = 5
16
+ attr_reader :last, :app, :host, :port, :server
17
+
18
+ def initialize app, port
19
+ @app = app
20
+ @host = "0.0.0.0"
21
+ @port = port
22
+ @server = Thin::Server.new(host, port.to_s) { run app }
23
+ mount
24
+ end
25
+
26
+ def host_and_port
27
+ "#{host}:#{port}"
28
+ end
29
+
30
+ def unmount
31
+ @server.stop
32
+ @server_thread.terminate
33
+ end
34
+
35
+ def mount
36
+ @server_thread = Thread.start do
37
+ server.start
38
+ end
39
+ Timeout.timeout(APPLICATION_START_TIMEOUT) do
40
+ loop_until { server.running? }
41
+ end
42
+ end
43
+
44
+ def get url, body, headers
45
+ @last = RestClient.get("#{host_and_port}#{url}", headers)
46
+ end
47
+
48
+ [:post, :put, :delete].each do |method|
49
+ define_method(method) do |url, body, headers|
50
+ @last = RestClient::Resource.new("#{host_and_port}#{url}", {}).send(method, body.to_s, headers)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def loop_until &block
57
+ loop { break if begin block.call rescue false end; sleep(1) }
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,27 @@
1
+ require 'contracted'
2
+ require 'active_support'
3
+
4
+ After do
5
+ Contracted.app and Contracted.app.unmount
6
+ end
7
+
8
+ Given /^the "([^"]*)" app is running$/ do |app_name|
9
+ app = Kernel.const_get(app_name).new
10
+ Contracted.mount app, '9898'
11
+ end
12
+
13
+ When /^an api client performs (GET|POST|PUT|DELETE) ([^\s]+)$/ do |method, url|
14
+ Contracted.app.send(method.downcase.to_sym, url, "", {})
15
+ end
16
+
17
+ When /^an api client performs (POST|PUT) ([^\s]+) with json body:$/ do |method, url, body|
18
+ Contracted.app.send(method.downcase.to_sym, url, body, {:content_type => 'application/json'})
19
+ end
20
+
21
+ Then /^the json response should look like:$/ do |response_body_string|
22
+ Contracted::ResponseDescription.new(response_body_string).should be_description_of Contracted.app.last
23
+ end
24
+
25
+ Then /^the json response body should look like:$/ do |response_body_string|
26
+ JsonDescriptionParser.new.parse(response_body_string).value.should == ActiveSupport::JSON.decode(Contracted.app.last.body)
27
+ end
@@ -0,0 +1,74 @@
1
+ grammar HttpDescription
2
+ rule http
3
+ status_line headers new_line body
4
+ end
5
+
6
+ rule body
7
+ .*
8
+ end
9
+
10
+ rule headers
11
+ header* (wildcard new_line)? {
12
+ def values
13
+ elements.collect {|e| e.elements }.flatten.compact.select {|e| e.respond_to?(:value) }.map(&:value)
14
+ end
15
+ }
16
+ end
17
+
18
+ rule wildcard
19
+ '...' {
20
+ def value
21
+ text_value
22
+ end
23
+ }
24
+ end
25
+
26
+ rule header
27
+ header_key ':' white_space header_value new_line {
28
+ def value
29
+ {header_key.text_value => header_value.text_value}
30
+ end
31
+ }
32
+ end
33
+
34
+ rule header_key
35
+ [^:]+
36
+ end
37
+
38
+ rule header_value
39
+ [^\n]+
40
+ end
41
+
42
+ rule status_line
43
+ 'HTTP/' version space status space reason new_line
44
+ end
45
+
46
+ rule version
47
+ [0-9.]+
48
+ end
49
+
50
+ rule white_space
51
+ [\s]+
52
+ end
53
+
54
+ rule space
55
+ ' '
56
+ end
57
+
58
+ rule reason_word
59
+ [A-Z]+
60
+ end
61
+
62
+ rule reason
63
+ reason_word (space reason_word)*
64
+ end
65
+
66
+ rule status
67
+ [0-9] 3..3
68
+ end
69
+
70
+ rule new_line
71
+ [\n]
72
+ end
73
+ end
74
+
@@ -0,0 +1,29 @@
1
+ module Contracted
2
+ module Json
3
+ class Wildcard
4
+ def self.instance
5
+ @@instance ||= Wildcard.new
6
+ end
7
+
8
+ def == other
9
+ true
10
+ end
11
+ end
12
+
13
+ class Object
14
+ def initialize elements
15
+ @hash = elements.select {|e| e != Wildcard.instance }.inject({}) {|r, h| r.merge(h)}
16
+ @include = elements.include? Wildcard.instance
17
+ end
18
+
19
+ def == other
20
+ if @include
21
+ other.merge(@hash) == other
22
+ else
23
+ @hash == other
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,128 @@
1
+ require 'contracted/json'
2
+
3
+ grammar JsonDescription
4
+ rule json
5
+ space json_value space {
6
+ def value
7
+ json_value.value
8
+ end
9
+ }
10
+ end
11
+
12
+ rule json_value
13
+ string / numeric / keyword / object / array / wildcard
14
+ end
15
+
16
+ rule string
17
+ '"' chars:char* '"' {
18
+ def value
19
+ chars.elements.map {|e| e.value }.join
20
+ end
21
+ }
22
+ end
23
+
24
+ rule wildcard
25
+ space '...' space {
26
+ def value
27
+ Contracted::Json::Wildcard.instance
28
+ end
29
+ }
30
+ end
31
+
32
+ rule char
33
+ !'"' ('\\\\' ( ( [nbfrt"] / '\\\\' / '/' ) / 'u' hex hex hex hex ) / !'\\\\' .) {
34
+ def value
35
+ if text_value[0..0] == '\\\\'
36
+ case c = text_value[1..1]
37
+ when /[nbfrt]/
38
+ {'n' => "\n", 'b' => "\b", 'f' => "\f", 'r' => "\r", 't' => "\t"}[c]
39
+ when 'u'
40
+ [text_value[2,4].to_i(16)].pack("U")
41
+ else
42
+ c
43
+ end
44
+ else
45
+ text_value
46
+ end
47
+ end
48
+ }
49
+ end
50
+
51
+ rule hex
52
+ [0-9a-fA-F]
53
+ end
54
+
55
+ rule numeric
56
+ exp / float / integer
57
+ end
58
+
59
+ rule exp
60
+ (float / integer) [eE] [+-]? unsigned_integer { def value; text_value.to_f; end }
61
+ end
62
+
63
+ rule float
64
+ integer '.' [0-9]+ { def value; text_value.to_f; end }
65
+ end
66
+
67
+ rule integer
68
+ '-'? unsigned_integer { def value; text_value.to_i; end }
69
+ end
70
+
71
+ rule unsigned_integer
72
+ ('0' / [1-9] [0-9]*)
73
+ end
74
+
75
+ rule keyword
76
+ ('true' / 'false' / 'null') {
77
+ def value
78
+ { 'true' => true, 'false' => false, 'null' => nil }[text_value]
79
+ end
80
+ }
81
+ end
82
+
83
+ rule object
84
+ '{' space object_elements:object_element* space '}' {
85
+ def value
86
+ Contracted::Json::Object.new(object_elements.elements.map {|p| p.value })
87
+ end
88
+ }
89
+ end
90
+
91
+ rule object_element
92
+ e:(pair / wildcard) (',' &object_element / !object_element) {
93
+ def value
94
+ e.value
95
+ end
96
+ }
97
+ end
98
+
99
+ rule pair
100
+ space string space ':' space json_value space {
101
+ def value
102
+ { string.value => json_value.value }
103
+ end
104
+ }
105
+ end
106
+
107
+ rule array
108
+ '[' space array_values:array_value* space ']' {
109
+ def value
110
+ array_values.elements.map {|e| e.value }
111
+ end
112
+ }
113
+ end
114
+
115
+ rule array_value
116
+ space json_value space (',' &array_value / !array_value) {
117
+ def value
118
+ json_value.value
119
+ end
120
+ }
121
+ end
122
+
123
+ rule space
124
+ [ \t\r\n]*
125
+ end
126
+
127
+ end
128
+
@@ -0,0 +1,3 @@
1
+ module Contracted
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'contracted/application'
3
+
4
+ describe "a rack app mounted with contracted.mount" do
5
+ before :all do @app = Contracted.mount(proc {|env| [200, {}, "body"]}, '9898') end
6
+
7
+ after :all do @app.unmount end
8
+
9
+ subject { @app }
10
+
11
+ its(:host_and_port) { should == '0.0.0.0:9898' }
12
+
13
+ it "should be able to perform GET operations" do
14
+ @app.get('/', '', {}).should == 'body'
15
+ end
16
+ end
17
+
18
+ describe "a rack app mounted with contracted.mount that returns a non 200 series status code" do
19
+ it "should timeout after waiting 5 seconds" do
20
+ Thin::Server.stub(:new).and_return(mock(:server, :running? => false))
21
+
22
+ Timeout.timeout(6) do
23
+ proc do
24
+ Contracted.mount(proc {|env| [500, {}, "body"]}, '9898')
25
+ end.should raise_error(Timeout::Error)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ require 'polyglot'
2
+ require 'treetop'
3
+ require 'contracted/http_description'
4
+
5
+ describe HttpDescriptionParser do
6
+ let(:description) { HttpDescriptionParser.new.parse(description_string) }
7
+
8
+ context "no headers as part of description" do
9
+ let(:description_string) { "HTTP/1.1 200 OK\n\n{'message': 'hello'}" }
10
+
11
+ describe "description" do
12
+ subject { description }
13
+
14
+ its(:headers) { subject.values.should == [] }
15
+ its(:headers) { subject.text_value.should == '' }
16
+ its(:body) { subject.text_value.should == "{'message': 'hello'}" }
17
+ end
18
+
19
+ describe "status line" do
20
+ subject { description.status_line }
21
+
22
+ its(:status) { subject.text_value.should == "200" }
23
+ its(:version) { subject.text_value.should == "1.1" }
24
+ its(:reason) { subject.text_value.should == "OK" }
25
+ end
26
+ end
27
+
28
+ context "wildcard header in description" do
29
+ let(:description_string) { "HTTP/1.1 200 OK\nContent-Type: application/json\n...\n\n{'message': 'hello'}" }
30
+
31
+ describe "description" do
32
+ subject { description }
33
+
34
+ its(:headers) { subject.values.should == [{'Content-Type' => 'application/json'}, '...'] }
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,46 @@
1
+ require 'active_support'
2
+ require 'contracted'
3
+
4
+ describe JsonDescriptionParser do
5
+ let(:parser) { JsonDescriptionParser.new }
6
+ let(:description) { parser.parse(description_string) }
7
+
8
+ subject { description.value }
9
+
10
+ context "a standard json representation" do
11
+ let(:description_string) { '{"message": "hello"}' }
12
+
13
+ describe "the description" do
14
+ it { should == json('{"message": "hello"}') }
15
+ it { should_not == json('{"message": "goodbye"}') }
16
+ end
17
+ end
18
+
19
+ context "with a key wildcard" do
20
+ let(:description_string) { '{"message": "hello", ...}' }
21
+
22
+ describe "the description" do
23
+ it { should == json('{"message": "hello"}') }
24
+ it { should == json('{"message": "hello", "type": "greeting"}') }
25
+ it { should_not == json('{"message": "goodbye"}') }
26
+ it { should_not == json('{"type": "greeting"}') }
27
+ end
28
+ end
29
+
30
+ context "with a wildcard value" do
31
+ let(:description_string) { '{"message": ..., "type": "greeting" }' }
32
+
33
+ describe "the description" do
34
+ it { should == json('{"message": "hello", "type": "greeting" }') }
35
+ it { should == json('{"message": "goodbye", "type": "greeting" }') }
36
+ it { should_not == json('{"msg": "hello", "type": "greeting" }') }
37
+ it { should_not == json('{"message": "hello"}') }
38
+ it { should_not == json('{"type": "greeting"}') }
39
+ end
40
+ end
41
+
42
+ def json json_string
43
+ ActiveSupport::JSON.decode json_string
44
+ end
45
+ end
46
+
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../features/support')
3
+
4
+ RSpec.configure do |config|
5
+ config.mock_with :rspec
6
+ end
metadata ADDED
@@ -0,0 +1,198 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api-testing
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Andrew Kiellor
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-05-31 00:00:00 +10:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: cucumber
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: thin
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rest-client
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: activesupport
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :runtime
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: rspec
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :runtime
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: treetop
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :runtime
104
+ version_requirements: *id006
105
+ - !ruby/object:Gem::Dependency
106
+ name: polyglot
107
+ prerelease: false
108
+ requirement: &id007 !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ type: :runtime
118
+ version_requirements: *id007
119
+ description: "\n Contracted is a tool aimed at testing JSON API's.\n "
120
+ email:
121
+ - akiellor@gmail.com
122
+ executables: []
123
+
124
+ extensions: []
125
+
126
+ extra_rdoc_files: []
127
+
128
+ files:
129
+ - .gitignore
130
+ - .rvmrc
131
+ - Gemfile
132
+ - README
133
+ - Rakefile
134
+ - contracted.gemspec
135
+ - features/echo.feature
136
+ - features/echo_body.feature
137
+ - features/hello_world.feature
138
+ - features/support/apps/echo.rb
139
+ - features/support/apps/echo_body.rb
140
+ - features/support/apps/hello_world.rb
141
+ - features/support/env.rb
142
+ - lib/contracted.rb
143
+ - lib/contracted/application.rb
144
+ - lib/contracted/cucumber.rb
145
+ - lib/contracted/http_description.treetop
146
+ - lib/contracted/json.rb
147
+ - lib/contracted/json_description.treetop
148
+ - lib/contracted/version.rb
149
+ - spec/contracted/application_spec.rb
150
+ - spec/contracted/http_description_parser_spec.rb
151
+ - spec/contracted/json_description_parser_spec.rb
152
+ - spec/spec_helper.rb
153
+ has_rdoc: true
154
+ homepage: ""
155
+ licenses: []
156
+
157
+ post_install_message:
158
+ rdoc_options: []
159
+
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ none: false
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ hash: 3
168
+ segments:
169
+ - 0
170
+ version: "0"
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ none: false
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ hash: 3
177
+ segments:
178
+ - 0
179
+ version: "0"
180
+ requirements: []
181
+
182
+ rubyforge_project: contracted
183
+ rubygems_version: 1.4.2
184
+ signing_key:
185
+ specification_version: 3
186
+ summary: Contracted is a tool aimed at testing JSON API's.
187
+ test_files:
188
+ - features/echo.feature
189
+ - features/echo_body.feature
190
+ - features/hello_world.feature
191
+ - features/support/apps/echo.rb
192
+ - features/support/apps/echo_body.rb
193
+ - features/support/apps/hello_world.rb
194
+ - features/support/env.rb
195
+ - spec/contracted/application_spec.rb
196
+ - spec/contracted/http_description_parser_spec.rb
197
+ - spec/contracted/json_description_parser_spec.rb
198
+ - spec/spec_helper.rb