api_sim 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +63 -0
- data/Rakefile +6 -0
- data/api_sim.gemspec +29 -0
- data/bin/console +31 -0
- data/bin/setup +8 -0
- data/config.example.ru +18 -0
- data/lib/api_sim/app_builder.rb +60 -0
- data/lib/api_sim/built_app.rb +145 -0
- data/lib/api_sim/matchers/base_matcher.rb +53 -0
- data/lib/api_sim/matchers/dynamic_request_matcher.rb +36 -0
- data/lib/api_sim/matchers/request_body_matcher.rb +38 -0
- data/lib/api_sim/matchers/static_request_matcher.rb +56 -0
- data/lib/api_sim/matchers.rb +35 -0
- data/lib/api_sim/public/app.css +7 -0
- data/lib/api_sim/public/app.js +42 -0
- data/lib/api_sim/public/favicon.ico +0 -0
- data/lib/api_sim/recorded_request.rb +20 -0
- data/lib/api_sim/version.rb +3 -0
- data/lib/api_sim/view_helpers.rb +41 -0
- data/lib/api_sim/views/index.html.erb +36 -0
- data/lib/api_sim/views/layout.html.erb +31 -0
- data/lib/api_sim/views/requests/index.html.erb +28 -0
- data/lib/api_sim/views/responses/form.html.erb +50 -0
- data/lib/api_sim.rb +10 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9a955753fef75d629e045f738959d4c4a523e466
|
4
|
+
data.tar.gz: fa3627d00573c6e0299fcc985738977bc40889bc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 71d6f5e74b37fc3f74e9ddfb45a7cea2804521d1cc52f579746478c3dc0d7c184b49c6625aed3e2880c7fe2b9b97b8d5c2bde529de0b898084469cd81e675961
|
7
|
+
data.tar.gz: 802e0105bae925cae7a1ca2a4af8ce272238ab3239cd47e3b991dc66482f8bbc85f14f8e474d2a9c8de2aabe3a57a904014b13b172612654f8df5e5d7aa14177
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 TJ Taylor
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# ApiSim
|
2
|
+
|
3
|
+

|
4
|
+
|
5
|
+
An HTTP API DSL on top of Sinatra.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'api_sim'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install api_sim
|
22
|
+
|
23
|
+
## Example usage
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require 'api_sim'
|
27
|
+
|
28
|
+
ENDPOINT_JSON_SCHEMA = {"type": "object", "properties": {"a": {"type": "integer"}}}.to_json
|
29
|
+
|
30
|
+
app = ApiSim.build_app do
|
31
|
+
configure_endpoint 'GET', '/endpoint', 'Hi!', 200, {'X-CUSTOM-HEADER' => 'easy as abc'}, ENDPOINT_JSON_SCHEMA
|
32
|
+
|
33
|
+
configure_dynamic_endpoint 'GET', '/dynamic', ->(req) {
|
34
|
+
[201, {'X-CUSTOM-HEADER' => '123'}, 'Howdy!']
|
35
|
+
}
|
36
|
+
|
37
|
+
configure_matcher_endpoint 'POST', '/soap', {
|
38
|
+
/Operation1/ => [200, {'Content-Type' => 'text/xml+soap'}, '<xml>Response1</xml>'],
|
39
|
+
/Operation2/ => [500, {'Content-Type' => 'text/xml+soap'}, '<xml>Response2</xml>'],
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
run app
|
44
|
+
```
|
45
|
+
|
46
|
+
The above is an exact copy of the `config.example.ru` from the root of the repo. You can boot this without too much
|
47
|
+
effort by running:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
bundle check || bundle install && bundle exec rackup -Ilib config.example.ru
|
51
|
+
```
|
52
|
+
|
53
|
+
After which the simulators should be running on port 9292.
|
54
|
+
|
55
|
+
## Development
|
56
|
+
|
57
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
58
|
+
|
59
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
60
|
+
|
61
|
+
## Contributing
|
62
|
+
|
63
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dugancathal/api_sim.
|
data/Rakefile
ADDED
data/api_sim.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'api_sim/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "api_sim"
|
8
|
+
spec.version = ApiSim::VERSION
|
9
|
+
spec.authors = ["TJ Taylor"]
|
10
|
+
spec.email = ["dugancathal@gmail.com"]
|
11
|
+
spec.licenses = ['MIT']
|
12
|
+
|
13
|
+
spec.summary = %q{A DSL on top of sinatra for building application simulators}
|
14
|
+
spec.description = %q{A DSL on top of sinatra for building application simulators}
|
15
|
+
spec.homepage = "https://github.com/dugancathal/api_sim"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "sinatra", '~> 1.0'
|
23
|
+
spec.add_dependency "nokogiri", '~> 1.6.7'
|
24
|
+
spec.add_dependency "json-schema", '~> 2.5.2'
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
spec.add_development_dependency "capybara", "~> 2.7.0"
|
29
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "api_sim"
|
5
|
+
|
6
|
+
puts <<-RUBY
|
7
|
+
#
|
8
|
+
# Welcome to ApiSim!
|
9
|
+
#
|
10
|
+
# You can boot a sample app by running the following
|
11
|
+
sample_app = ApiSim.build_app do
|
12
|
+
configure_endpoint 'GET' , '/my-endpoint', 'Returns Hello!', 202
|
13
|
+
end
|
14
|
+
|
15
|
+
# You can inspect the endpoints that this app has:
|
16
|
+
puts "=" * 50, sample_app.endpoints, "=" * 50
|
17
|
+
|
18
|
+
# You can run that app one of two ways.
|
19
|
+
# Synchronously (you'll need to open a new window to play with the server):
|
20
|
+
sample_app.run!
|
21
|
+
|
22
|
+
# Asynchronously (to continue playing in this window):
|
23
|
+
Thread.new { sample_app.run! }
|
24
|
+
|
25
|
+
# You can stop the synchronous app by pressing CTRL+c.
|
26
|
+
# If running asynchronously, you can stop by simply exiting the console.
|
27
|
+
#
|
28
|
+
RUBY
|
29
|
+
|
30
|
+
require "irb"
|
31
|
+
IRB.start
|
data/bin/setup
ADDED
data/config.example.ru
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'api_sim'
|
2
|
+
|
3
|
+
ENDPOINT_JSON_SCHEMA = {"type": "object", "properties": {"a": {"type": "integer"}}}.to_json
|
4
|
+
|
5
|
+
app = ApiSim.build_app do
|
6
|
+
configure_endpoint 'GET', '/endpoint', 'Hi!', 200, {'X-CUSTOM-HEADER' => 'easy as abc'}, ENDPOINT_JSON_SCHEMA
|
7
|
+
|
8
|
+
configure_dynamic_endpoint 'GET', '/dynamic', ->(req) {
|
9
|
+
[201, {'X-CUSTOM-HEADER' => '123'}, 'Howdy!']
|
10
|
+
}
|
11
|
+
|
12
|
+
configure_matcher_endpoint 'POST', '/soap', {
|
13
|
+
/Operation1/ => [200, {'Content-Type' => 'text/xml+soap'}, '<xml>Response1</xml>'],
|
14
|
+
/Operation2/ => [500, {'Content-Type' => 'text/xml+soap'}, '<xml>Response2</xml>'],
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
run app
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'api_sim/built_app'
|
2
|
+
require 'api_sim/matchers'
|
3
|
+
|
4
|
+
module ApiSim
|
5
|
+
class AppBuilder
|
6
|
+
NOT_FOUND = [nil, [404, {}, 'NOT FOUND']]
|
7
|
+
|
8
|
+
def rackapp
|
9
|
+
config = self
|
10
|
+
Class.new(BuiltApp) do
|
11
|
+
endpoints config.endpoint_configurations
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure_endpoint(http_method, route, response_body, response_code=200, headers={}, schema_string='')
|
16
|
+
endpoint_configurations.push(
|
17
|
+
Matchers::StaticRequestMatcher.new(
|
18
|
+
http_method: http_method,
|
19
|
+
route: route,
|
20
|
+
response_code: response_code,
|
21
|
+
headers: headers,
|
22
|
+
default: true,
|
23
|
+
response_body: response_body,
|
24
|
+
schema: schema_string
|
25
|
+
)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def configure_dynamic_endpoint(http_method, route, response_logic)
|
30
|
+
endpoint_configurations.push(
|
31
|
+
Matchers::DynamicRequestMatcher.new(
|
32
|
+
http_method: http_method,
|
33
|
+
route: route,
|
34
|
+
default: true,
|
35
|
+
response_generator: response_logic
|
36
|
+
)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def configure_matcher_endpoint(http_method, route, matchers_to_responses)
|
41
|
+
matchers_to_responses.each do |matcher, response|
|
42
|
+
endpoint_configurations.push(
|
43
|
+
Matchers::RequestBodyMatcher.new(
|
44
|
+
http_method: http_method,
|
45
|
+
route: route,
|
46
|
+
response_code: response[0],
|
47
|
+
headers: response[1],
|
48
|
+
response_body: response[2],
|
49
|
+
default: true,
|
50
|
+
body_matches: matcher
|
51
|
+
)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def endpoint_configurations
|
57
|
+
@endpoint_configurations ||= []
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'json'
|
4
|
+
require 'tilt/erb'
|
5
|
+
require 'api_sim/view_helpers'
|
6
|
+
require 'json-schema'
|
7
|
+
|
8
|
+
module ApiSim
|
9
|
+
class BuiltApp < Sinatra::Base
|
10
|
+
use Rack::MethodOverride
|
11
|
+
|
12
|
+
helpers do
|
13
|
+
include ViewHelpers
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.endpoints(endpoints = nil)
|
17
|
+
return @endpoints if @endpoints
|
18
|
+
@endpoints = endpoints
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/' do
|
22
|
+
erb :'index.html', layout: :'layout.html'
|
23
|
+
end
|
24
|
+
|
25
|
+
get '/ui/response/:method/*' do
|
26
|
+
@config = matcher(faux_request(http_method, route, faux_body))
|
27
|
+
erb :'responses/form.html', layout: :'layout.html'
|
28
|
+
end
|
29
|
+
|
30
|
+
get '/ui/requests/:method/*' do
|
31
|
+
@config = matcher(faux_request(http_method, route, faux_body))
|
32
|
+
|
33
|
+
erb :'requests/index.html', layout: :'layout.html'
|
34
|
+
end
|
35
|
+
|
36
|
+
post '/ui/response/:method/*' do
|
37
|
+
@config = matcher(faux_request(http_method, route, faux_body))
|
38
|
+
unless params['schema'].empty?
|
39
|
+
@errors = JSON::Validator.fully_validate(params['schema'], params['body'])
|
40
|
+
if @errors.any?
|
41
|
+
return erb :'responses/form.html', layout: :'layout.html'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
new_config = create_matcher_override(mimicked_request)
|
46
|
+
|
47
|
+
self.class.endpoints.unshift(new_config)
|
48
|
+
redirect to '/'
|
49
|
+
end
|
50
|
+
|
51
|
+
delete '/ui/response/:method/*' do
|
52
|
+
all_matching_matchers = matchers(faux_request(http_method, route, request.body))
|
53
|
+
all_matching_matchers.each &:reset!
|
54
|
+
non_default_matchers = all_matching_matchers.reject(&:default)
|
55
|
+
self.class.endpoints.delete_if { |endpoint| non_default_matchers.include?(endpoint) }
|
56
|
+
redirect to '/'
|
57
|
+
end
|
58
|
+
|
59
|
+
put '/response/*' do
|
60
|
+
self.class.endpoints.unshift(create_matcher_override(mimicked_request))
|
61
|
+
''
|
62
|
+
end
|
63
|
+
|
64
|
+
delete '/response/*' do
|
65
|
+
all_matching_matchers = matchers(mimicked_request)
|
66
|
+
all_matching_matchers.each &:reset!
|
67
|
+
non_default_matchers = all_matching_matchers.reject(&:default)
|
68
|
+
self.class.endpoints.delete_if { |endpoint| non_default_matchers.include?(endpoint) }
|
69
|
+
''
|
70
|
+
end
|
71
|
+
|
72
|
+
%i(get post put patch delete).each do |http_method|
|
73
|
+
public_send(http_method, '/*') do
|
74
|
+
endpoint = matcher(request)
|
75
|
+
endpoint.record_request(request)
|
76
|
+
endpoint.response(request)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def create_matcher_override(request)
|
83
|
+
old_matcher = matcher(request)
|
84
|
+
config = matcher_overrides(old_matcher.response(request))
|
85
|
+
old_matcher.overridden!
|
86
|
+
Matcher.dupe_and_reconfigure(old_matcher, config)
|
87
|
+
end
|
88
|
+
|
89
|
+
def matcher_overrides(old_config)
|
90
|
+
parsed_body.merge(
|
91
|
+
response_code: parsed_body.fetch('status', old_config[0]).to_i,
|
92
|
+
headers: parsed_body.fetch('headers', old_config[1]),
|
93
|
+
response_body: parsed_body.fetch('body', old_config[2]),
|
94
|
+
matcher: parsed_body.fetch('match', ''),
|
95
|
+
schema: parsed_body.fetch('schema', '')
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def mimicked_request
|
100
|
+
faux_request(http_method, route, request.body)
|
101
|
+
end
|
102
|
+
|
103
|
+
def http_method
|
104
|
+
parsed_body.fetch('method', params['method']).upcase
|
105
|
+
end
|
106
|
+
|
107
|
+
def matcher(request)
|
108
|
+
matchers(request).first || halt(404)
|
109
|
+
end
|
110
|
+
|
111
|
+
def matchers(request)
|
112
|
+
self.class.endpoints.select { |matcher| matcher.matches?(request) }
|
113
|
+
end
|
114
|
+
|
115
|
+
def faux_request(method='', path='', body=StringIO.new(''))
|
116
|
+
body.rewind
|
117
|
+
Rack::Request.new({'rack.input' => body, 'REQUEST_METHOD' => method, 'PATH_INFO' => path})
|
118
|
+
end
|
119
|
+
|
120
|
+
def faux_body
|
121
|
+
StringIO.new(params[:match].to_s)
|
122
|
+
end
|
123
|
+
|
124
|
+
def parsed_body
|
125
|
+
return @response_body if @response_body
|
126
|
+
|
127
|
+
@response_body = case request.env['CONTENT_TYPE']
|
128
|
+
when 'application/json' then
|
129
|
+
JSON.parse(request.body.read)
|
130
|
+
when 'text/xml' then
|
131
|
+
Nokogiri::XML(request.body.read)
|
132
|
+
when 'application/x-www-form-urlencoded' then
|
133
|
+
params
|
134
|
+
else
|
135
|
+
if request.path =~ /ui/
|
136
|
+
params
|
137
|
+
else
|
138
|
+
request.body.read
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
@response_body.empty? ? {} : @response_body
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'api_sim/recorded_request'
|
2
|
+
|
3
|
+
module ApiSim
|
4
|
+
module Matchers
|
5
|
+
class BaseMatcher
|
6
|
+
DEFAULT_RACK_RESPONSE=[200, {}, '']
|
7
|
+
ALWAYS_TRUE_MATCHER = ->(request) { true }
|
8
|
+
|
9
|
+
def custom_matcher?
|
10
|
+
matcher != ALWAYS_TRUE_MATCHER
|
11
|
+
end
|
12
|
+
|
13
|
+
def overridden!
|
14
|
+
@overridden = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def overridden?
|
18
|
+
!!@overridden
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset!
|
22
|
+
@overridden = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def requests
|
26
|
+
@requests ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def match_on_body?
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def readonly?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def record_request(request)
|
38
|
+
request.body.rewind
|
39
|
+
requests.push(RecordedRequest.new(body: request.body.read, request_env: request.env))
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
<<-DOC.gsub(/^\s+/, '')
|
44
|
+
#{http_method} #{route}
|
45
|
+
DOC
|
46
|
+
end
|
47
|
+
|
48
|
+
def response(_)
|
49
|
+
[response_code, headers, response_body]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'api_sim/recorded_request'
|
2
|
+
require 'api_sim/matchers/base_matcher'
|
3
|
+
|
4
|
+
module ApiSim
|
5
|
+
module Matchers
|
6
|
+
class DynamicRequestMatcher < BaseMatcher
|
7
|
+
attr_reader :response_generator, :route, :http_method, :default, :matcher
|
8
|
+
|
9
|
+
def initialize(http_method:, route:, response_generator:, default: false, matcher: ALWAYS_TRUE_MATCHER)
|
10
|
+
@matcher = matcher
|
11
|
+
@route = route
|
12
|
+
@http_method = http_method
|
13
|
+
@default = default
|
14
|
+
@response_generator = response_generator
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(request)
|
18
|
+
request.path == route && request.request_method == http_method && matcher.call(request)
|
19
|
+
end
|
20
|
+
|
21
|
+
def response(request)
|
22
|
+
response_generator.call(request)
|
23
|
+
end
|
24
|
+
|
25
|
+
def readonly?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
<<-DOC.gsub(/^\s+/, '')
|
31
|
+
#{http_method} #{route} -> DYNAMIC BASED ON REQUEST
|
32
|
+
DOC
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'api_sim/recorded_request'
|
2
|
+
require 'api_sim/matchers/base_matcher'
|
3
|
+
|
4
|
+
module ApiSim
|
5
|
+
module Matchers
|
6
|
+
class RequestBodyMatcher < BaseMatcher
|
7
|
+
attr_reader :http_method, :route, :headers, :response_code, :matcher, :response_body, :default, :schema
|
8
|
+
|
9
|
+
def initialize(http_method:, route:, response_code: 200, response_body: '', headers: {}, default: false, body_matches:, schema: nil)
|
10
|
+
@default = default
|
11
|
+
@matcher = Regexp.compile(body_matches)
|
12
|
+
@headers = headers
|
13
|
+
@response_body = response_body
|
14
|
+
@response_code = response_code
|
15
|
+
@route = route
|
16
|
+
@http_method = http_method
|
17
|
+
@schema = schema
|
18
|
+
end
|
19
|
+
|
20
|
+
def matches?(request)
|
21
|
+
request.body.rewind
|
22
|
+
body = request.body.read
|
23
|
+
request.body.rewind
|
24
|
+
request.path == route && request.request_method == http_method && matcher.match(body)
|
25
|
+
end
|
26
|
+
|
27
|
+
def match_on_body?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
<<-DOC.gsub(/^\s+/, '')
|
33
|
+
#{http_method} #{route} /#{matcher.source}/ -> (#{response_code}) #{response_body[0..20]}...
|
34
|
+
DOC
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'api_sim/recorded_request'
|
2
|
+
require 'api_sim/matchers/base_matcher'
|
3
|
+
|
4
|
+
module ApiSim
|
5
|
+
module Matchers
|
6
|
+
class StaticRequestMatcher < BaseMatcher
|
7
|
+
attr_reader :http_method, :route, :headers, :response_code, :matcher, :response_body, :default, :schema
|
8
|
+
|
9
|
+
def initialize(http_method:, route:, response_code: 200, response_body: '', headers: {}, default: false, matcher: ALWAYS_TRUE_MATCHER, schema: nil)
|
10
|
+
@default = default
|
11
|
+
@matcher = matcher
|
12
|
+
@headers = headers
|
13
|
+
@response_body = response_body
|
14
|
+
@response_code = response_code
|
15
|
+
@route = route
|
16
|
+
@http_method = http_method
|
17
|
+
@schema = schema
|
18
|
+
end
|
19
|
+
|
20
|
+
def matches?(request)
|
21
|
+
request.path == route && request.request_method == http_method && matcher.call(request)
|
22
|
+
end
|
23
|
+
|
24
|
+
def custom_matcher?
|
25
|
+
matcher != ALWAYS_TRUE_MATCHER
|
26
|
+
end
|
27
|
+
|
28
|
+
def overridden!
|
29
|
+
@overridden = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def overridden?
|
33
|
+
!!@overridden
|
34
|
+
end
|
35
|
+
|
36
|
+
def reset!
|
37
|
+
@overridden = false
|
38
|
+
end
|
39
|
+
|
40
|
+
def requests
|
41
|
+
@requests ||= []
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
<<-DOC.gsub(/^\s+/, '')
|
46
|
+
#{http_method} #{route} -> (#{response_code}) #{response_body[0..20]}...
|
47
|
+
DOC
|
48
|
+
end
|
49
|
+
|
50
|
+
def record_request(request)
|
51
|
+
request.body.rewind
|
52
|
+
requests.push(RecordedRequest.new(body: request.body.read, request_env: request.env))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'api_sim/matchers/dynamic_request_matcher'
|
2
|
+
require 'api_sim/matchers/static_request_matcher'
|
3
|
+
require 'api_sim/matchers/request_body_matcher'
|
4
|
+
|
5
|
+
module ApiSim
|
6
|
+
class Matcher
|
7
|
+
OVERRIDE_CLASS_MAP = {
|
8
|
+
Matchers::DynamicRequestMatcher => Matchers::StaticRequestMatcher,
|
9
|
+
Matchers::StaticRequestMatcher => Matchers::StaticRequestMatcher,
|
10
|
+
Matchers::RequestBodyMatcher => Matchers::RequestBodyMatcher,
|
11
|
+
}
|
12
|
+
|
13
|
+
def self.dupe_and_reconfigure(old_matcher, overrides)
|
14
|
+
if old_matcher.match_on_body?
|
15
|
+
Matchers::RequestBodyMatcher.new(
|
16
|
+
route: old_matcher.route,
|
17
|
+
http_method: old_matcher.http_method,
|
18
|
+
response_code: overrides.fetch(:response_code),
|
19
|
+
headers: overrides.fetch(:headers),
|
20
|
+
response_body: overrides.fetch(:response_body),
|
21
|
+
body_matches: overrides.fetch('matcher', old_matcher.matcher)
|
22
|
+
)
|
23
|
+
else
|
24
|
+
Matchers::StaticRequestMatcher.new(
|
25
|
+
route: old_matcher.route,
|
26
|
+
http_method: old_matcher.http_method,
|
27
|
+
response_code: overrides.fetch(:response_code),
|
28
|
+
headers: overrides.fetch(:headers),
|
29
|
+
response_body: overrides.fetch(:response_body),
|
30
|
+
schema: overrides.fetch(:schema),
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
(function () {
|
2
|
+
var prettifyJson = function prettifyJson(obj) {
|
3
|
+
return JSON.stringify(obj, null, 2);
|
4
|
+
};
|
5
|
+
|
6
|
+
var prettyPrintAllThings = function prettyPrintAllThings() {
|
7
|
+
var elements = document.querySelectorAll('[data-prettify]');
|
8
|
+
for (var i = 0; i < elements.length; i++) {
|
9
|
+
var el = elements[i];
|
10
|
+
try {
|
11
|
+
obj = JSON.parse(el.value);
|
12
|
+
el.value = prettifyJson(obj)
|
13
|
+
} catch (e) {
|
14
|
+
}
|
15
|
+
}
|
16
|
+
};
|
17
|
+
|
18
|
+
var allowCollapseOfTableRows = function allowCollapseOfTableRows() {
|
19
|
+
var elements = document.querySelectorAll('tr');
|
20
|
+
for (var i = 0; i < elements.length; i++) {
|
21
|
+
var el = elements[i];
|
22
|
+
|
23
|
+
el.onclick = function collapseTableRow(event) {
|
24
|
+
var tr = event.target.parentNode;
|
25
|
+
var lastTds = Array.prototype.slice.apply(tr.children).slice(1);
|
26
|
+
lastTds.forEach(function (td) {
|
27
|
+
var tdContent = Array.prototype.slice.apply(td.children);
|
28
|
+
tdContent.forEach(function (innerElement) {
|
29
|
+
if (innerElement.style.display) {
|
30
|
+
innerElement.style.display = null;
|
31
|
+
} else {
|
32
|
+
innerElement.style.display = 'none';
|
33
|
+
}
|
34
|
+
});
|
35
|
+
});
|
36
|
+
}
|
37
|
+
}
|
38
|
+
};
|
39
|
+
|
40
|
+
prettyPrintAllThings();
|
41
|
+
allowCollapseOfTableRows();
|
42
|
+
})();
|
Binary file
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ApiSim
|
2
|
+
class RecordedRequest
|
3
|
+
attr_reader :time, :headers, :body
|
4
|
+
|
5
|
+
def initialize(time: Time.now, body:, request_env:)
|
6
|
+
@time = time
|
7
|
+
@body = body
|
8
|
+
@headers = parse_headers_from(request_env)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def parse_headers_from(request_env)
|
13
|
+
request_env.select do |k, v|
|
14
|
+
k =~ /^HTTP_/
|
15
|
+
end.each_with_object({}) do |(k, v), h|
|
16
|
+
h[k.split('_')[1..-1].join('-')] = v
|
17
|
+
end.sort
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ApiSim
|
2
|
+
module ViewHelpers
|
3
|
+
def endpoints
|
4
|
+
self.class.endpoints.reject(&:overridden?).sort_by { |endpoint| [endpoint.http_method, endpoint.route].join(' ') }
|
5
|
+
end
|
6
|
+
|
7
|
+
def custom_matcher?(endpoint)
|
8
|
+
endpoint.custom_matcher? ? '(Custom matcher)' : ''
|
9
|
+
end
|
10
|
+
|
11
|
+
def config
|
12
|
+
@config
|
13
|
+
end
|
14
|
+
|
15
|
+
def route
|
16
|
+
"/#{params[:splat].first}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def link_to_response_edit(endpoint)
|
20
|
+
match = endpoint.match_on_body? ? endpoint.matcher.source : ''
|
21
|
+
<<-HTML
|
22
|
+
<a href="/ui/response/#{endpoint.http_method}#{endpoint.route}?match=#{match}">
|
23
|
+
#{endpoint.route}
|
24
|
+
</a>
|
25
|
+
HTML
|
26
|
+
end
|
27
|
+
|
28
|
+
def h(text)
|
29
|
+
Rack::Utils.escape_html(text)
|
30
|
+
end
|
31
|
+
|
32
|
+
def link_to_read_requests(endpoint)
|
33
|
+
match = endpoint.match_on_body? ? endpoint.matcher.source : ''
|
34
|
+
<<-HTML
|
35
|
+
<a href="/ui/requests/#{endpoint.http_method}#{endpoint.route}?match=#{match}">
|
36
|
+
#{endpoint.requests.count}
|
37
|
+
</a>
|
38
|
+
HTML
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
<h2>Simulators</h2>
|
2
|
+
<table>
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th>Simulated endpoint</th>
|
6
|
+
<th>Body Match?</th>
|
7
|
+
<th>Response</th>
|
8
|
+
<th>Requests</th>
|
9
|
+
<th></th>
|
10
|
+
</tr>
|
11
|
+
</thead>
|
12
|
+
<tbody>
|
13
|
+
<% endpoints.each do |endpoint| %>
|
14
|
+
<tr>
|
15
|
+
<td><%= endpoint.http_method %> <%= endpoint.route %> <%= custom_matcher?(endpoint) %></td>
|
16
|
+
<td><%= endpoint.match_on_body? ? "/#{endpoint.matcher.source}/" : '' %></td>
|
17
|
+
<td>
|
18
|
+
<% if endpoint.readonly? %>
|
19
|
+
Cannot edit dynamic endpoints
|
20
|
+
<% else %>
|
21
|
+
<%= link_to_response_edit endpoint %>
|
22
|
+
<% end %>
|
23
|
+
</td>
|
24
|
+
<td>
|
25
|
+
<%= link_to_read_requests endpoint %>
|
26
|
+
</td>
|
27
|
+
<td>
|
28
|
+
<form action="/ui/response/<%= endpoint.http_method %><%= endpoint.route %>" method="post">
|
29
|
+
<input type="hidden" value="delete" name="_method">
|
30
|
+
<button class='btn-link' type="submit">Reset</button>
|
31
|
+
</form>
|
32
|
+
</td>
|
33
|
+
</tr>
|
34
|
+
<% end %>
|
35
|
+
</tbody>
|
36
|
+
</table>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
4
|
+
|
5
|
+
<!-- Optional theme -->
|
6
|
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
7
|
+
<link rel="stylesheet" href="/app.css">
|
8
|
+
|
9
|
+
<style>
|
10
|
+
body {
|
11
|
+
padding: 20px;
|
12
|
+
}
|
13
|
+
|
14
|
+
table {
|
15
|
+
border-collapse: collapse;
|
16
|
+
}
|
17
|
+
|
18
|
+
td, th {
|
19
|
+
padding: 10px;
|
20
|
+
text-align: left;
|
21
|
+
border: 1px solid gray;
|
22
|
+
}
|
23
|
+
</style>
|
24
|
+
</head>
|
25
|
+
<body>
|
26
|
+
<div class="container">
|
27
|
+
<%= yield %>
|
28
|
+
<script src="/app.js"></script>
|
29
|
+
</div>
|
30
|
+
</body>
|
31
|
+
</html>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1>Simulators</h1>
|
3
|
+
<h2>Requests to <%= config.http_method %> <%= config.route %></h2>
|
4
|
+
</div>
|
5
|
+
<table class="table">
|
6
|
+
<thead>
|
7
|
+
<tr>
|
8
|
+
<th>Time</th>
|
9
|
+
<th>Body</th>
|
10
|
+
<th>Headers</th>
|
11
|
+
</tr>
|
12
|
+
</thead>
|
13
|
+
<tbody>
|
14
|
+
<% config.requests.each do |request| %>
|
15
|
+
<tr>
|
16
|
+
<th class="header-column"><p><%= request.time %></p></th>
|
17
|
+
<td><p><%= h request.body %></p></td>
|
18
|
+
<td>
|
19
|
+
<ul>
|
20
|
+
<% request.headers.each do |header, value| %>
|
21
|
+
<li><%= header %>: <%= value %></li>
|
22
|
+
<% end %>
|
23
|
+
</ul>
|
24
|
+
</td>
|
25
|
+
</tr>
|
26
|
+
<% end %>
|
27
|
+
</tbody>
|
28
|
+
</table>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1>Simulators</h1>
|
3
|
+
<h2>Response for <%= config.http_method %> <%= config.route %></h2>
|
4
|
+
</div>
|
5
|
+
<form action="/ui/response/<%= config.http_method %><%= config.route %>" method="post">
|
6
|
+
<div class="row">
|
7
|
+
<% if @errors %>
|
8
|
+
<div class="alert alert-danger">
|
9
|
+
<p>Body does not match expected schema</p>
|
10
|
+
<ul>
|
11
|
+
<% @errors.each do |error| %>
|
12
|
+
<li><%= error %></li>
|
13
|
+
<% end %>
|
14
|
+
</ul>
|
15
|
+
</div>
|
16
|
+
<% end %>
|
17
|
+
</div>
|
18
|
+
<div class="row">
|
19
|
+
<div class="col-md-6">
|
20
|
+
<div class="form-group">
|
21
|
+
<label for="status">Status code</label>
|
22
|
+
<input type="text" class="form-control" id="status" name="status" value="<%= params[:status] || config.response_code %>">
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<% if config.match_on_body? %>
|
26
|
+
<div class="form-group">
|
27
|
+
<label for="match">Match body on</label>
|
28
|
+
<input type="text" class="form-control" id="match" name="match" disabled="disabled" value="<%= params[:match] || config.matcher.source %>"/>
|
29
|
+
<input type="hidden" id="match" name="match" value="<%= params[:match] || config.matcher.source %>"/>
|
30
|
+
</div>
|
31
|
+
<% end %>
|
32
|
+
|
33
|
+
<div class="form-group">
|
34
|
+
<label for="schema">Response schema</label>
|
35
|
+
<textarea type="text" class="form-control" id="schema" name="schema" data-prettify rows="25"><%= params['schema'] || config.schema %></textarea>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<div class="col-md-6">
|
40
|
+
<div class="form-group">
|
41
|
+
<label for="body">Response body</label>
|
42
|
+
<textarea class="form-control" id="body" name="body" data-prettify rows="25"><%= params['body'] || config.response_body %></textarea>
|
43
|
+
</div>
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<div class="form-actions pull-right">
|
47
|
+
<button class='btn btn-primary' type="submit">Save</button>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</form>
|
data/lib/api_sim.rb
ADDED
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_sim
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- TJ Taylor
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.6.7
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.6.7
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json-schema
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.5.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.5.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.11'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.11'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: capybara
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.7.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.7.0
|
111
|
+
description: A DSL on top of sinatra for building application simulators
|
112
|
+
email:
|
113
|
+
- dugancathal@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- ".travis.yml"
|
121
|
+
- Gemfile
|
122
|
+
- LICENSE.txt
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- api_sim.gemspec
|
126
|
+
- bin/console
|
127
|
+
- bin/setup
|
128
|
+
- config.example.ru
|
129
|
+
- lib/api_sim.rb
|
130
|
+
- lib/api_sim/app_builder.rb
|
131
|
+
- lib/api_sim/built_app.rb
|
132
|
+
- lib/api_sim/matchers.rb
|
133
|
+
- lib/api_sim/matchers/base_matcher.rb
|
134
|
+
- lib/api_sim/matchers/dynamic_request_matcher.rb
|
135
|
+
- lib/api_sim/matchers/request_body_matcher.rb
|
136
|
+
- lib/api_sim/matchers/static_request_matcher.rb
|
137
|
+
- lib/api_sim/public/app.css
|
138
|
+
- lib/api_sim/public/app.js
|
139
|
+
- lib/api_sim/public/favicon.ico
|
140
|
+
- lib/api_sim/recorded_request.rb
|
141
|
+
- lib/api_sim/version.rb
|
142
|
+
- lib/api_sim/view_helpers.rb
|
143
|
+
- lib/api_sim/views/index.html.erb
|
144
|
+
- lib/api_sim/views/layout.html.erb
|
145
|
+
- lib/api_sim/views/requests/index.html.erb
|
146
|
+
- lib/api_sim/views/responses/form.html.erb
|
147
|
+
homepage: https://github.com/dugancathal/api_sim
|
148
|
+
licenses:
|
149
|
+
- MIT
|
150
|
+
metadata: {}
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
require_paths:
|
154
|
+
- lib
|
155
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
requirements: []
|
166
|
+
rubyforge_project:
|
167
|
+
rubygems_version: 2.5.1
|
168
|
+
signing_key:
|
169
|
+
specification_version: 4
|
170
|
+
summary: A DSL on top of sinatra for building application simulators
|
171
|
+
test_files: []
|