api_sim 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api_sim.gemspec
4
+ gemspec
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
+ ![Build Tag](https://travis-ci.org/dugancathal/api_sim.svg?branch=master)
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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,7 @@
1
+ form {
2
+ margin: 0;
3
+ }
4
+
5
+ .header-column {
6
+ max-width: 12em;
7
+ }
@@ -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,3 @@
1
+ module ApiSim
2
+ VERSION = "0.1.0"
3
+ 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
@@ -0,0 +1,10 @@
1
+ require "api_sim/version"
2
+ require 'api_sim/app_builder'
3
+
4
+ module ApiSim
5
+ def self.build_app(&block)
6
+ configuration = AppBuilder.new
7
+ configuration.instance_eval &block
8
+ configuration.rackapp
9
+ end
10
+ end
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: []