doppelserver 0.4.9 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +39 -46
- data/README.md +122 -23
- data/bin/console +2 -2
- data/bin/scc.rb +90 -0
- data/doppelserver.gemspec +2 -2
- data/lib/doppelserver.rb +1 -2
- data/lib/doppelserver/base_server.rb +4 -140
- data/lib/doppelserver/data.rb +18 -1
- data/lib/doppelserver/routes/control.rb +59 -0
- data/lib/doppelserver/routes/helpers/capture_helper.rb +19 -0
- data/lib/doppelserver/routes/helpers/pluralize_helper.rb +35 -0
- data/lib/doppelserver/routes/helpers/request_parse_helper.rb +37 -0
- data/lib/doppelserver/routes/rest.rb +89 -0
- data/lib/doppelserver/service_controller.rb +75 -0
- data/lib/doppelserver/version.rb +1 -1
- metadata +14 -5
- data/bin/server +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b1b219ac52c0632c708b0187d92484a5a1c4881aa80b6cec2e59c317eb8ad7b
|
4
|
+
data.tar.gz: 4384f653e8558901121b2a47484a7677b4819dff7b1f5c9e1cc4d638a6b222b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4b89cf1685aff54633bfd56f0c70c415de73a9481f033b60a229bd76278f610c979e296823f716a1454fef1aae43a8b5dca285143e1bd81f2e5fce9b3c2561e
|
7
|
+
data.tar.gz: 1f4f8ee391a7431c4982143897d2be8c2df6754049c3a688e8f9496749c0caabfdfe52feedb3e7e20880c9da568e6792b56126e9322fd5e06b46c8d9de3034cd
|
data/.rubocop_todo.yml
CHANGED
@@ -1,77 +1,70 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2018-
|
3
|
+
# on 2018-07-12 03:45:51 -0700 using RuboCop version 0.58.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
8
|
|
9
|
-
# Offense count:
|
9
|
+
# Offense count: 7
|
10
10
|
# Configuration parameters: CountComments, ExcludedMethods.
|
11
|
+
# ExcludedMethods: refine
|
11
12
|
Metrics/BlockLength:
|
12
13
|
Max: 116
|
13
14
|
|
14
|
-
# Offense count: 2
|
15
|
-
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
16
|
-
# URISchemes: http, https
|
17
|
-
Metrics/LineLength:
|
18
|
-
Max: 82
|
19
|
-
|
20
15
|
# Offense count: 1
|
21
|
-
|
22
|
-
Exclude:
|
23
|
-
- 'lib/doppelserver/base_server.rb'
|
24
|
-
|
25
|
-
# Offense count: 2
|
26
|
-
Style/Documentation:
|
27
|
-
Exclude:
|
28
|
-
- 'spec/**/*'
|
29
|
-
- 'test/**/*'
|
30
|
-
- 'lib/doppelserver/base_server.rb'
|
31
|
-
- 'lib/doppelserver/data.rb'
|
32
|
-
|
33
|
-
# Offense count: 3
|
34
|
-
# Cop supports --auto-correct.
|
35
|
-
Style/ParallelAssignment:
|
16
|
+
RSpec/DescribeClass:
|
36
17
|
Exclude:
|
37
|
-
- '
|
18
|
+
- 'spec/service_controller_console_spec.rb'
|
38
19
|
|
39
|
-
# Offense count:
|
40
|
-
#
|
41
|
-
|
42
|
-
# SupportedStyles: implicit, explicit
|
43
|
-
Style/RescueStandardError:
|
20
|
+
# Offense count: 5
|
21
|
+
# Configuration parameters: Max.
|
22
|
+
RSpec/ExampleLength:
|
44
23
|
Exclude:
|
45
|
-
- '
|
24
|
+
- 'spec/base_server_spec.rb'
|
46
25
|
|
47
|
-
# Offense count:
|
48
|
-
|
49
|
-
# Configuration parameters: AllowIfMethodIsEmpty.
|
50
|
-
Style/SingleLineMethods:
|
26
|
+
# Offense count: 6
|
27
|
+
RSpec/ExpectInHook:
|
51
28
|
Exclude:
|
52
29
|
- 'spec/base_server_spec.rb'
|
30
|
+
- 'spec/service_controller_console_spec.rb'
|
53
31
|
|
54
32
|
# Offense count: 1
|
33
|
+
# Configuration parameters: CustomTransform, IgnoreMethods.
|
55
34
|
RSpec/FilePath:
|
56
35
|
Exclude:
|
57
36
|
- 'spec/base_server_spec.rb'
|
58
37
|
|
59
|
-
# Offense count:
|
60
|
-
|
61
|
-
|
62
|
-
|
38
|
+
# Offense count: 14
|
39
|
+
# Configuration parameters: AggregateFailuresByDefault.
|
40
|
+
RSpec/MultipleExpectations:
|
41
|
+
Max: 4
|
63
42
|
|
64
|
-
# Offense count:
|
65
|
-
RSpec/
|
43
|
+
# Offense count: 1
|
44
|
+
RSpec/NestedGroups:
|
45
|
+
Max: 4
|
46
|
+
|
47
|
+
# Offense count: 1
|
48
|
+
Style/ClassVars:
|
66
49
|
Exclude:
|
67
|
-
- '
|
50
|
+
- 'lib/doppelserver/base_server.rb'
|
68
51
|
|
69
|
-
# Offense count:
|
70
|
-
|
52
|
+
# Offense count: 1
|
53
|
+
Style/Documentation:
|
71
54
|
Exclude:
|
72
|
-
- 'spec
|
55
|
+
- 'spec/**/*'
|
56
|
+
- 'test/**/*'
|
57
|
+
- 'lib/doppelserver/base_server.rb'
|
73
58
|
|
74
|
-
# Offense
|
75
|
-
|
59
|
+
# Offense count: 1
|
60
|
+
# Cop supports --auto-correct.
|
61
|
+
# Configuration parameters: AllowIfMethodIsEmpty.
|
62
|
+
Style/SingleLineMethods:
|
76
63
|
Exclude:
|
77
64
|
- 'spec/base_server_spec.rb'
|
65
|
+
|
66
|
+
# Offense count: 1
|
67
|
+
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
68
|
+
# URISchemes: http, https
|
69
|
+
Metrics/LineLength:
|
70
|
+
Max: 81
|
data/README.md
CHANGED
@@ -7,15 +7,22 @@
|
|
7
7
|
[![Gem Version](https://badge.fury.io/rb/doppelserver.svg)](https://badge.fury.io/rb/doppelserver)
|
8
8
|
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/dd50d7ee18ae46c38ad053cf3dc59794)](https://www.codacy.com/app/drewcoo/doppelserver?utm_source=github.com&utm_medium=referral&utm_content=drewcoo/doppelserver&utm_campaign=Badge_Grade)
|
9
9
|
|
10
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/doppelserver`. To experiment with that code, run `bin/console` for an interactive prompt.
|
11
10
|
|
12
|
-
|
11
|
+
## What's Doppelerver?
|
13
12
|
|
14
|
-
|
13
|
+
Welcome to Doppelserver, the fake RESTful service gem, so named because all the cool names were already taken on RubyGems.
|
15
14
|
|
16
|
-
|
15
|
+
Chances are you're already using some kind of double in your testing. Test
|
16
|
+
doubles fit into several categories:
|
17
|
+
- stubs - objects with values chosen to make tests pass
|
18
|
+
- mocks - objects with running code that's set to make tests pass
|
19
|
+
- spies - objects that can be queried for what happened to them
|
20
|
+
- fakes - actual running objects but incomplete and instrumented
|
21
|
+
|
22
|
+
Martin Fowler [has a good explanation](https://martinfowler.com/articles/mocksArentStubs.html) but he separates what I'm calling stubs into "stubs" that respond with what a test needs and "dummies" that are passed but never accessed.
|
23
|
+
|
24
|
+
In this case we're talking about a fake service, an actual running service intended to replace what you'd normally have in production for test purposes. It should respond as the actual service would but is instrumented so that you can both ask what happened like a spy and, more importantly, tell it how to respond differently because it's a fake.
|
17
25
|
|
18
|
-
Also, all the cool names were taken on RubyGems.
|
19
26
|
|
20
27
|
## Installation
|
21
28
|
|
@@ -31,23 +38,114 @@ Or install it yourself as:
|
|
31
38
|
|
32
39
|
$ gem install doppelserver
|
33
40
|
|
41
|
+
|
34
42
|
## Usage
|
35
43
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
44
|
+
|
45
|
+
### Manually Exploring
|
46
|
+
There's a command line tool, scc.rb, to explore what Doppelserver does:
|
47
|
+
|
48
|
+
$ bin/scc.rb help
|
49
|
+
Commands:
|
50
|
+
scc.rb data # send/retreive all data
|
51
|
+
scc.rb help [COMMAND] # Describe available commands or one specific command
|
52
|
+
scc.rb restart # restart server, dumping all data
|
53
|
+
scc.rb start # start server
|
54
|
+
scc.rb stop # stop server
|
55
|
+
|
56
|
+
Options:
|
57
|
+
-p, [--port=PORT] # port service runs on
|
58
|
+
# Default: 7357
|
59
|
+
|
60
|
+
|
61
|
+
#### Basics
|
62
|
+
|
63
|
+
So let's just start it, defaulting to port 7357 (which is leet speak for "test").
|
64
|
+
|
65
|
+
$ bin/scc.rb start
|
66
|
+
|
67
|
+
It's now running and you can use your favorite tool to run RESTful queries on it. I'll suggest [Postman](https://www.getpostman.com/) if you don't have anything. The fake service understands plural collections and items in them with (non-negative) integer ids. So you can get the initial things collection:
|
68
|
+
|
69
|
+
GET http://localhost:7357/things
|
70
|
+
|
71
|
+
It returns a 200 but no data because it's currently empty. Let's put something in it.
|
72
|
+
|
73
|
+
POST http://localhost:7357/things
|
74
|
+
and send this data:
|
75
|
+
{ "name": "first", "another_value": "second" }
|
76
|
+
|
77
|
+
Now we get a 200 back because we succeeded. Succeeded at what? Let's see:
|
78
|
+
|
79
|
+
GET http://localhost:7357/things
|
80
|
+
|
81
|
+
That 200s and returns this:
|
82
|
+
|
83
|
+
{
|
84
|
+
"0": {
|
85
|
+
"name": "first",
|
86
|
+
"another_value": "second"
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
You can also retrieve by index.
|
91
|
+
|
92
|
+
GET http://localhost:7357/things/0
|
93
|
+
returns:
|
94
|
+
{
|
95
|
+
"name": "first",
|
96
|
+
"another_value": "second"
|
97
|
+
}
|
98
|
+
|
99
|
+
In general, this will behave like an in-memory CRUD store. That starts and stops and restarts. So much for basics . . .
|
100
|
+
|
101
|
+
$ bin/scc.rb stop
|
102
|
+
|
103
|
+
|
104
|
+
#### Controlling the Data
|
105
|
+
|
106
|
+
There is a control endpoint that does more for us.
|
107
|
+
|
108
|
+
|
109
|
+
##### Running
|
110
|
+
|
111
|
+
Yes, this returns { "status": "running" } if running:
|
112
|
+
|
113
|
+
GET http://localhost:7537/control
|
114
|
+
|
115
|
+
|
116
|
+
##### Data
|
117
|
+
|
118
|
+
You can look at or change all of the data either with the /control/data endpoint or with the command line tool's data command.
|
119
|
+
|
120
|
+
If you want to have the server not respond do this:
|
121
|
+
1. get data (optionally save somewhere)
|
122
|
+
2. stop service
|
123
|
+
3. run query-to-fail against service
|
124
|
+
4. start service
|
125
|
+
5. restore data
|
126
|
+
|
127
|
+
|
128
|
+
##### Version Numbers Et Al.
|
129
|
+
|
130
|
+
To support urls with things like version numbers the service already supports urls like this:
|
131
|
+
|
132
|
+
http://localhost/<ADDITONAL_STRING>/collection/1
|
133
|
+
|
134
|
+
Where <ADDITIONAL_STRING> is any string. That means GETting and POSTing these is the same:
|
135
|
+
|
136
|
+
http://localhost/collection/0
|
137
|
+
http://localhost/FOO/collection/0
|
138
|
+
http://localhost/v1/BAR/BAZ/collection/0
|
139
|
+
|
140
|
+
|
141
|
+
## TODO
|
142
|
+
|
143
|
+
I should still add this functionality:
|
144
|
+
- return non-REST data (possibly non-HTTP)
|
145
|
+
- GraphQL? Some subset of it?
|
146
|
+
- Others? WSDL?
|
147
|
+
- Client bindings? POROs?
|
148
|
+
|
51
149
|
|
52
150
|
## Development
|
53
151
|
|
@@ -55,18 +153,19 @@ After checking out the repo, run `bundle` to install dependencies. Then, run `bu
|
|
55
153
|
|
56
154
|
To install this gem onto your local machine, run
|
57
155
|
|
58
|
-
bundle exec rake install
|
156
|
+
$ bundle exec rake install
|
59
157
|
|
60
158
|
To release a new version, update the version number in `version.rb` like so
|
61
159
|
|
62
|
-
bundle exec gem bump -v [major|minor|patch|pre|release]
|
160
|
+
$ bundle exec gem bump -v [major|minor|patch|pre|release]
|
63
161
|
|
64
162
|
and then run
|
65
163
|
|
66
|
-
bundle exec rake release
|
164
|
+
$ bundle exec rake release
|
67
165
|
|
68
166
|
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).
|
69
167
|
|
168
|
+
|
70
169
|
## Contributing
|
71
170
|
|
72
171
|
Bug reports and pull requests are welcome on GitHub at https://github.com/drewcoo/doppelserver.
|
data/bin/console
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
2
|
+
$LOAD_PATH.unshift File.expand_path 'lib', __dir__
|
3
3
|
require 'bundler/setup'
|
4
|
-
require '
|
4
|
+
require 'doppelserver'
|
5
5
|
|
6
6
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
data/bin/scc.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.expand_path '../lib', __dir__
|
3
|
+
require 'rubygems'
|
4
|
+
require 'doppelserver'
|
5
|
+
require 'thor'
|
6
|
+
|
7
|
+
#
|
8
|
+
# A command line tool to tell the Doppelserver service controller
|
9
|
+
# what to do.
|
10
|
+
#
|
11
|
+
class ServiceControllerConsole < Thor
|
12
|
+
map %w[/? /h] => :help
|
13
|
+
map 'up' => :start
|
14
|
+
map 'down' => :stop
|
15
|
+
|
16
|
+
DEFAULT_PORT = '7357'.freeze
|
17
|
+
class_option :port, default: DEFAULT_PORT, aliases: '-p',
|
18
|
+
desc: 'port service runs on'
|
19
|
+
|
20
|
+
desc 'helper: bail_out!', 'abort with a message', hide: true
|
21
|
+
def bail_out!
|
22
|
+
message = running? ? 'already' : 'not'
|
23
|
+
abort "ERROR #{message} running on port #{options.port}"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'helper: running?', 'Bool am I running?', hide: true
|
27
|
+
def running?
|
28
|
+
# memoize this so that we don't call running? twice
|
29
|
+
# when we 'bail_out! if runnning?' and we exit this tool after each call
|
30
|
+
# so the memoization doesn't hurt x-call
|
31
|
+
#
|
32
|
+
# I used this, so left it commented in case I need it later:
|
33
|
+
# puts defined?(@running) ? 'defined' : 'not defined'
|
34
|
+
@running ||= service_controller.running?
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'helper: service controller', 'tell the service what to do', hide: true
|
38
|
+
def service_controller
|
39
|
+
@service_controller ||= ServiceController.new(options.port)
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Also hide this method. It's the one that starts the server
|
44
|
+
# as the current process. The start method starts a new process
|
45
|
+
# invoking this.
|
46
|
+
#
|
47
|
+
desc 'server', 'do not call directly', hide: true
|
48
|
+
def server
|
49
|
+
service_controller.serve
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'start', 'start server'
|
53
|
+
method_option :data, aliases: '-d',
|
54
|
+
desc: 'text JSON data - remember to escape quotes'
|
55
|
+
def start
|
56
|
+
bail_out! if running?
|
57
|
+
service_controller.start
|
58
|
+
set unless options.data.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
desc 'stop', 'stop server'
|
62
|
+
method_option :data, aliases: '-d',
|
63
|
+
desc: 'if passed, dumps data to console'
|
64
|
+
def stop
|
65
|
+
bail_out! unless running?
|
66
|
+
data if options.data
|
67
|
+
service_controller.stop
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'restart', 'restart server, dumping all data'
|
71
|
+
def restart
|
72
|
+
bail_out! unless running?
|
73
|
+
service_controller.restart
|
74
|
+
end
|
75
|
+
|
76
|
+
# Bad data zeroes everything out. Fix that or just doc it in help?
|
77
|
+
desc 'data', 'send/retreive all data'
|
78
|
+
method_option :set, aliases: '-s',
|
79
|
+
desc: '-s <data> to send JSON; no args to get from server'
|
80
|
+
def data
|
81
|
+
bail_out! unless running?
|
82
|
+
if options.set
|
83
|
+
service_controller.set(options.set)
|
84
|
+
else
|
85
|
+
STDOUT.puts service_controller.get.gsub('"', '\"')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
ServiceControllerConsole.start
|
data/doppelserver.gemspec
CHANGED
@@ -33,8 +33,8 @@ Gem::Specification.new do |spec|
|
|
33
33
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
34
34
|
f.match(%r{^(test|spec|features)/})
|
35
35
|
end
|
36
|
-
spec.bindir = '
|
37
|
-
spec.executables = spec.files.grep(%r{^
|
36
|
+
spec.bindir = 'bin'
|
37
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
38
38
|
spec.require_paths = ['lib']
|
39
39
|
|
40
40
|
spec.add_development_dependency 'activesupport', '~> 5.0'
|
data/lib/doppelserver.rb
CHANGED
@@ -1,35 +1,11 @@
|
|
1
1
|
require 'sinatra'
|
2
|
-
require '
|
3
|
-
#
|
4
|
-
|
2
|
+
require 'doppelserver/data'
|
3
|
+
# Most of the BaseServer class is in the routes below:
|
4
|
+
require 'doppelserver/routes/control'
|
5
|
+
require 'doppelserver/routes/rest'
|
5
6
|
|
6
7
|
module Doppelserver
|
7
|
-
ENFORCE_PLURALS = true
|
8
|
-
|
9
8
|
class BaseServer < Sinatra::Base
|
10
|
-
require 'active_support' # for singularization/pluralization
|
11
|
-
|
12
|
-
#
|
13
|
-
# Determines if
|
14
|
-
# word:: an input word
|
15
|
-
# is singular.
|
16
|
-
# Retutns truthy.
|
17
|
-
#
|
18
|
-
def singular?(word)
|
19
|
-
ActiveSupport::Inflector.singularize(word) == word
|
20
|
-
end
|
21
|
-
|
22
|
-
#
|
23
|
-
# If ENFORCE_PLURALS is true (default == true)
|
24
|
-
# This enforces plural names for all collections
|
25
|
-
# and returns 403 otherwise.
|
26
|
-
# word:: collection name
|
27
|
-
#
|
28
|
-
def enforce_plural(word)
|
29
|
-
return unless ENFORCE_PLURALS
|
30
|
-
halt 403, 'only plural collection names allowed' if singular?(word)
|
31
|
-
end
|
32
|
-
|
33
9
|
#
|
34
10
|
# Hold all of the data internally in the @@data.
|
35
11
|
# It's the Data class, which holds a hash of data and
|
@@ -39,118 +15,6 @@ module Doppelserver
|
|
39
15
|
@@data = Data.new
|
40
16
|
end
|
41
17
|
|
42
|
-
#
|
43
|
-
# All meta-operations should happen through a /control endpoint.
|
44
|
-
#
|
45
|
-
# Deletes the internal data by clearing the @@data hash.
|
46
|
-
#
|
47
|
-
delete '/control/data' do
|
48
|
-
@@data.clear
|
49
|
-
end
|
50
|
-
|
51
|
-
#
|
52
|
-
# TODO:: Define this.
|
53
|
-
# Maybe make it a list of collections?
|
54
|
-
# Or all collections with all their data?
|
55
|
-
#
|
56
|
-
# For now, it returns a placeholder string.
|
57
|
-
#
|
58
|
-
get '/' do
|
59
|
-
'here i am' # TODO: Change this.
|
60
|
-
end
|
61
|
-
|
62
|
-
#
|
63
|
-
# Gets the data at:
|
64
|
-
# endpoint:: the collection
|
65
|
-
# id:: the instance identifier
|
66
|
-
# Or 404s when not found.
|
67
|
-
#
|
68
|
-
get '/:endpoint/:id' do
|
69
|
-
collection, id = params['endpoint'], params['id']
|
70
|
-
enforce_plural(collection)
|
71
|
-
halt 404 unless @@data.collection?(collection)
|
72
|
-
result = @@data.get_value(collection, id)
|
73
|
-
halt 404 if result.nil?
|
74
|
-
json(result)
|
75
|
-
end
|
76
|
-
|
77
|
-
#
|
78
|
-
# Returns the data at:
|
79
|
-
# endpoint:: collection name
|
80
|
-
# This is all of the data in that collection
|
81
|
-
# or 404 if no endpoint.
|
82
|
-
#
|
83
|
-
get '/:endpoint' do
|
84
|
-
collection = params[:endpoint]
|
85
|
-
enforce_plural(collection)
|
86
|
-
halt 404 unless @@data.collection?(collection)
|
87
|
-
json(@@data.get_collection(collection))
|
88
|
-
end
|
89
|
-
|
90
|
-
#
|
91
|
-
# No PUT, POST.
|
92
|
-
# Adds data to collection:
|
93
|
-
# endpoint:: is the name of the collection.
|
94
|
-
# If it doesn't exist, add new collection.
|
95
|
-
# If it does exist replaces (???) data.
|
96
|
-
# Also 403 on no data passed.
|
97
|
-
#
|
98
|
-
post '/:endpoint' do
|
99
|
-
request.body.rewind
|
100
|
-
collection = params['endpoint']
|
101
|
-
enforce_plural(collection)
|
102
|
-
raw_data = request.body.read
|
103
|
-
halt 403, 'NO DATA' if raw_data.empty?
|
104
|
-
data = begin
|
105
|
-
JSON.parse(raw_data)
|
106
|
-
rescue
|
107
|
-
halt 403, "BAD DATA:\n#{raw_data}"
|
108
|
-
end
|
109
|
-
json(id: @@data.add(collection, data))
|
110
|
-
end
|
111
|
-
|
112
|
-
#
|
113
|
-
# No PUT, POST.
|
114
|
-
# Add data with the id to the collection.
|
115
|
-
# endpoint:: collection name
|
116
|
-
# id:: unique identifier for the data
|
117
|
-
# Adds a collection if doesn't exist.
|
118
|
-
# Adds id and data at it if id doesn't exist.
|
119
|
-
# Otherwise, replaces data. Unless no data, then 403.
|
120
|
-
#
|
121
|
-
post '/:endpoint/:id' do
|
122
|
-
request.body.rewind
|
123
|
-
collection, id = params['endpoint'], params['id']
|
124
|
-
enforce_plural(collection)
|
125
|
-
halt 403 unless @@data.collection_key?(collection, id)
|
126
|
-
raw_data = request.body.read
|
127
|
-
halt 403, 'NO DATA' if raw_data.empty?
|
128
|
-
data = begin
|
129
|
-
JSON.parse(raw_data)
|
130
|
-
rescue
|
131
|
-
halt 403, "BAD DATA:\n#{raw_data}"
|
132
|
-
end
|
133
|
-
halt 403 unless @@data.update?(collection, id, data)
|
134
|
-
end
|
135
|
-
|
136
|
-
#
|
137
|
-
# Delete the data from
|
138
|
-
# endpoint:: collection name
|
139
|
-
# id:: with unique identifier
|
140
|
-
# Or 404 if one of those isn't found.
|
141
|
-
#
|
142
|
-
delete '/:endpoint/:id' do
|
143
|
-
request.body.rewind
|
144
|
-
collection, id = params['endpoint'], params['id']
|
145
|
-
enforce_plural(collection)
|
146
|
-
halt 404 unless @@data.collection?(collection)
|
147
|
-
if @@data.delete(collection, id)
|
148
|
-
status 200
|
149
|
-
else
|
150
|
-
halt 404
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
18
|
run! if app_file == $PROGRAM_NAME
|
155
19
|
end
|
156
20
|
end
|
data/lib/doppelserver/data.rb
CHANGED
@@ -4,7 +4,12 @@ module Doppelserver
|
|
4
4
|
#
|
5
5
|
class Data
|
6
6
|
#
|
7
|
-
#
|
7
|
+
# @data will be COLLECTION : { 0: { HASH }, 1: { HASH }, ... }
|
8
|
+
# with the number autoincrementing by default
|
9
|
+
# @next_keys is the hash of COLLECTION names and the next auto-
|
10
|
+
# incrementing value
|
11
|
+
#
|
12
|
+
# And now it's all empty.
|
8
13
|
#
|
9
14
|
def initialize
|
10
15
|
@data = {}
|
@@ -34,6 +39,18 @@ module Doppelserver
|
|
34
39
|
collection?(collection) && @data[collection].key?(key)
|
35
40
|
end
|
36
41
|
|
42
|
+
def hash
|
43
|
+
{ 'data' => @data,
|
44
|
+
'next_keys' => @next_keys }
|
45
|
+
end
|
46
|
+
alias export hash
|
47
|
+
|
48
|
+
def hash=(hash)
|
49
|
+
@data = hash['data']
|
50
|
+
@next_keys = hash['next_keys']
|
51
|
+
end
|
52
|
+
alias import hash=
|
53
|
+
|
37
54
|
#
|
38
55
|
# Returns collection named parameter name.
|
39
56
|
#
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sinatra/json'
|
3
|
+
require 'doppelserver/routes/helpers/request_parse_helper'
|
4
|
+
|
5
|
+
module Doppelserver
|
6
|
+
#
|
7
|
+
# The control routes for BaseServer
|
8
|
+
#
|
9
|
+
class BaseServer < Sinatra::Base
|
10
|
+
include RequestParseHelper
|
11
|
+
#
|
12
|
+
# All meta-operations should happen through a /control endpoint.
|
13
|
+
#
|
14
|
+
# DELETEs the internal data by clearing the @@data hash.
|
15
|
+
#
|
16
|
+
delete '/control/data' do
|
17
|
+
@@data.clear
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# GET all of the data.
|
22
|
+
#
|
23
|
+
get '/control/data' do
|
24
|
+
json(@@data.export)
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Replace all of the data.
|
29
|
+
# GET the data, stop the service, restart the service, and POST the data
|
30
|
+
# and we have the service in the same state as is was.
|
31
|
+
#
|
32
|
+
post '/control/data' do
|
33
|
+
request.body.rewind
|
34
|
+
@@data.hash = read_post_data(request.body)
|
35
|
+
status 200
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# TODO:: Define this.
|
40
|
+
# Maybe make it a list of collections?
|
41
|
+
# Or all collections with all their data?
|
42
|
+
#
|
43
|
+
# For now, it returns a placeholder string.
|
44
|
+
#
|
45
|
+
get '/control' do
|
46
|
+
json('status': 'running')
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# DELETE on the /control endpoint to stop the service
|
51
|
+
# and Kernel.exit! stops immedately, skipping even at_exit stuff.
|
52
|
+
#
|
53
|
+
delete '/control' do
|
54
|
+
# Save state first or count on caller to prep for the service
|
55
|
+
# losing all data?
|
56
|
+
exit!
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# Capture helper - regexen.
|
3
|
+
#
|
4
|
+
module CaptureHelper
|
5
|
+
# Seems evil but it works. :-(
|
6
|
+
# these do pattern matching.
|
7
|
+
# They parse out what they say they do while allowing ANY prefix.
|
8
|
+
# So http://localhost/v3/myserver/route/id works just like
|
9
|
+
# http://localhost/route/id
|
10
|
+
#
|
11
|
+
# Because this seems wrong, I'm leaving it notably ugly so that I'll notice
|
12
|
+
# and maybe improve it later.
|
13
|
+
#
|
14
|
+
MATCH_NONSLASH = /([^\/]+)/
|
15
|
+
MATCH_NUMBER = /([\d]+)/
|
16
|
+
# These start with optional everythingm followed bu slash-separated matches
|
17
|
+
COLLECTION_ONLY = /.*\/#{MATCH_NONSLASH}\/?/ # possible trailing slash
|
18
|
+
COLLECTION_AND_ID = /.*\/#{MATCH_NONSLASH}\/#{MATCH_NUMBER}/
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#
|
2
|
+
# Like the name suggests, helper methods for pluralization.
|
3
|
+
#
|
4
|
+
module PluralizeHelper
|
5
|
+
require 'active_support'
|
6
|
+
|
7
|
+
ENFORCE_PLURALS = true
|
8
|
+
|
9
|
+
#
|
10
|
+
# Determines if
|
11
|
+
# word:: an input word
|
12
|
+
# is singular.
|
13
|
+
# Retutns truthy.
|
14
|
+
#
|
15
|
+
def singular?(word)
|
16
|
+
ActiveSupport::Inflector.singularize(word) == word
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# If ENFORCE_PLURALS is true (default == true)
|
21
|
+
# This enforces plural names for all collections
|
22
|
+
# and returns 403 otherwise.
|
23
|
+
# word:: collection name
|
24
|
+
#
|
25
|
+
def enforce_plural(word)
|
26
|
+
# Mixing in the contol exclusion is strange here.
|
27
|
+
# This can still coexist with a /controls endpoint but may be confusing.
|
28
|
+
# I don't want to think about the possibility that we don't have plural
|
29
|
+
# collections and happen to have a collision with a control collection yet.
|
30
|
+
if word != 'control' && ENFORCE_PLURALS && singular?(word)
|
31
|
+
halt 403, 'only plural collection names allowed'
|
32
|
+
end
|
33
|
+
word
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'doppelserver/routes/helpers/pluralize_helper'
|
2
|
+
#
|
3
|
+
# Helper methods for web request parsing.
|
4
|
+
#
|
5
|
+
module RequestParseHelper
|
6
|
+
include PluralizeHelper
|
7
|
+
#
|
8
|
+
# Parses parameters and checks that collections are plural if needed.
|
9
|
+
#
|
10
|
+
def parse_params(params)
|
11
|
+
collection, id = params['captures']
|
12
|
+
enforce_plural(collection)
|
13
|
+
id ? [collection, id] : collection
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Also for non-POST, callers ask to check the collection against
|
18
|
+
# known collections and 404 if asking for an unknown one
|
19
|
+
#
|
20
|
+
def check_exists(collection, data)
|
21
|
+
halt 404 unless data.collection?(collection)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Takes a request body and returns post data.
|
26
|
+
# Unless is errors out because of no data or a parse error.
|
27
|
+
#
|
28
|
+
def read_post_data(body)
|
29
|
+
raw_data = body.read
|
30
|
+
halt 403, 'NO DATA' if raw_data.empty?
|
31
|
+
begin
|
32
|
+
JSON.parse(raw_data)
|
33
|
+
rescue JSON::ParserError
|
34
|
+
halt 403, "BAD DATA:\n#{raw_data}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sinatra/json'
|
3
|
+
require 'doppelserver/routes/helpers/capture_helper'
|
4
|
+
require 'doppelserver/routes/helpers/request_parse_helper'
|
5
|
+
|
6
|
+
module Doppelserver
|
7
|
+
#
|
8
|
+
# All of the REST routes for BaseServer are here.
|
9
|
+
#
|
10
|
+
class BaseServer < Sinatra::Base
|
11
|
+
include CaptureHelper
|
12
|
+
include RequestParseHelper
|
13
|
+
|
14
|
+
#
|
15
|
+
# Gets the data at:
|
16
|
+
# endpoint:: the collection
|
17
|
+
# id:: the instance identifier
|
18
|
+
# Or 404s when not found.
|
19
|
+
#
|
20
|
+
get COLLECTION_AND_ID do
|
21
|
+
collection, id = parse_params(params)
|
22
|
+
check_exists(collection, @@data)
|
23
|
+
result = @@data.get_value(collection, id)
|
24
|
+
halt 404 if result.nil?
|
25
|
+
json(result)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Returns the data at:
|
30
|
+
# endpoint:: collection name
|
31
|
+
# This is all of the data in that collection
|
32
|
+
# or 404 if no endpoint.
|
33
|
+
#
|
34
|
+
get COLLECTION_ONLY do
|
35
|
+
collection = parse_params(params)
|
36
|
+
check_exists(collection, @@data)
|
37
|
+
json(@@data.get_collection(collection))
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# No PUT, POST.
|
42
|
+
# Add data with the id to the collection.
|
43
|
+
# endpoint:: collection name
|
44
|
+
# id:: unique identifier for the data
|
45
|
+
# Adds a collection if doesn't exist.
|
46
|
+
# Adds id and data at it if id doesn't exist.
|
47
|
+
# Otherwise, replaces data. Unless no data, then 403.
|
48
|
+
#
|
49
|
+
post COLLECTION_AND_ID do
|
50
|
+
request.body.rewind
|
51
|
+
collection, id = parse_params(params)
|
52
|
+
halt 403 unless @@data.collection_key?(collection, id)
|
53
|
+
data = read_post_data(request.body)
|
54
|
+
halt 403 unless @@data.update?(collection, id, data)
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# No PUT, POST.
|
59
|
+
# Adds data to collection:
|
60
|
+
# endpoint:: is the name of the collection.
|
61
|
+
# If it doesn't exist, add new collection.
|
62
|
+
# If it does exist replaces (???) data.
|
63
|
+
# Also 403 on no data passed.
|
64
|
+
#
|
65
|
+
post COLLECTION_ONLY do
|
66
|
+
request.body.rewind
|
67
|
+
collection = parse_params(params)
|
68
|
+
data = read_post_data(request.body)
|
69
|
+
json(id: @@data.add(collection, data))
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Delete the data from
|
74
|
+
# endpoint:: collection name
|
75
|
+
# id:: with unique identifier
|
76
|
+
# Or 404 if one of those isn't found.
|
77
|
+
#
|
78
|
+
delete COLLECTION_AND_ID do
|
79
|
+
request.body.rewind
|
80
|
+
collection, id = parse_params(params)
|
81
|
+
check_exists(collection, @@data)
|
82
|
+
if @@data.delete(collection, id)
|
83
|
+
status 200
|
84
|
+
else
|
85
|
+
halt 404
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('..', __dir__)
|
2
|
+
require 'doppelserver/base_server'
|
3
|
+
require 'faraday'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Contains the commands to tell the service what to do.
|
8
|
+
# All of the command line parsing and such is in a /bin/ that I haven't
|
9
|
+
# decided what to name yet.
|
10
|
+
#
|
11
|
+
class ServiceController
|
12
|
+
SERVER = 'http://127.0.0.1'
|
13
|
+
|
14
|
+
attr_reader :port
|
15
|
+
|
16
|
+
def initialize(port)
|
17
|
+
@port = port
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
command = "ruby #{__FILE__} server #{@port}"
|
22
|
+
command = if ENV['OS'] == 'Windows_NT'
|
23
|
+
"start \"doppelserver\" cmd /c #{command}"
|
24
|
+
else
|
25
|
+
"nohup #{command} &"
|
26
|
+
end
|
27
|
+
system command
|
28
|
+
sleep 0.1 until running?
|
29
|
+
end
|
30
|
+
|
31
|
+
def serve
|
32
|
+
system "title doppelserver - port #{@port}" if ENV['OS'] == 'Windows_NT'
|
33
|
+
server = Doppelserver::BaseServer
|
34
|
+
server.port = @port
|
35
|
+
server.run!
|
36
|
+
end
|
37
|
+
|
38
|
+
# rubocop:disable Lint/HandleExceptions
|
39
|
+
def stop
|
40
|
+
return unless running?
|
41
|
+
Faraday.delete "#{SERVER}:#{@port}/control"
|
42
|
+
rescue Faraday::ConnectionFailed
|
43
|
+
# return 0
|
44
|
+
end
|
45
|
+
# rubocop:enable Lint/HandleExceptions
|
46
|
+
|
47
|
+
def restart
|
48
|
+
stop
|
49
|
+
start
|
50
|
+
end
|
51
|
+
|
52
|
+
def running?
|
53
|
+
response = Faraday.get "#{SERVER}:#{@port}/control"
|
54
|
+
JSON.parse(response.body)['status'] == 'running'
|
55
|
+
rescue Faraday::ConnectionFailed
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
def get
|
60
|
+
response = Faraday.get "#{SERVER}:#{@port}/control/data"
|
61
|
+
response.body
|
62
|
+
end
|
63
|
+
|
64
|
+
def set(data)
|
65
|
+
Faraday.post "#{SERVER}:#{@port}/control/data", data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# If this, then turn this process into a runnning service.
|
71
|
+
#
|
72
|
+
if $PROGRAM_NAME == __FILE__ && ARGV[0] == 'server'
|
73
|
+
server = ServiceController.new(ARGV[1])
|
74
|
+
server.serve
|
75
|
+
end
|
data/lib/doppelserver/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: doppelserver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Drew Cooper
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-07-
|
11
|
+
date: 2018-07-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -212,7 +212,10 @@ description: |-
|
|
212
212
|
Unlike mocks and stubs, fake services are running processes that pretend to function as real ones would. This enables testing the software under test with more complete control of the surfaces it talks to (other services, faked). Beyond that, it makes application- and (http)protocol-level fault injection easy.
|
213
213
|
email:
|
214
214
|
- drewcoo@gmail.com
|
215
|
-
executables:
|
215
|
+
executables:
|
216
|
+
- console
|
217
|
+
- scc.rb
|
218
|
+
- setup
|
216
219
|
extensions: []
|
217
220
|
extra_rdoc_files: []
|
218
221
|
files:
|
@@ -229,12 +232,18 @@ files:
|
|
229
232
|
- Rakefile
|
230
233
|
- appveyor.yml
|
231
234
|
- bin/console
|
232
|
-
- bin/
|
235
|
+
- bin/scc.rb
|
233
236
|
- bin/setup
|
234
237
|
- doppelserver.gemspec
|
235
238
|
- lib/doppelserver.rb
|
236
239
|
- lib/doppelserver/base_server.rb
|
237
240
|
- lib/doppelserver/data.rb
|
241
|
+
- lib/doppelserver/routes/control.rb
|
242
|
+
- lib/doppelserver/routes/helpers/capture_helper.rb
|
243
|
+
- lib/doppelserver/routes/helpers/pluralize_helper.rb
|
244
|
+
- lib/doppelserver/routes/helpers/request_parse_helper.rb
|
245
|
+
- lib/doppelserver/routes/rest.rb
|
246
|
+
- lib/doppelserver/service_controller.rb
|
238
247
|
- lib/doppelserver/version.rb
|
239
248
|
homepage: https://github.com/drewcoo/doppelserver
|
240
249
|
licenses:
|
data/bin/server
DELETED