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 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: []