file_sv 0.1.0 → 0.1.5
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 +4 -4
- data/exe/file_sv +12 -4
- data/lib/file_sv.rb +23 -14
- data/lib/file_sv/file_sv.ico +0 -0
- data/lib/file_sv/global_settings.rb +23 -0
- data/lib/file_sv/planned_endpoint.rb +15 -6
- data/lib/file_sv/service_loader.rb +9 -1
- data/lib/file_sv/sv_plan.rb +36 -14
- data/lib/file_sv/version.rb +1 -1
- data/lib/file_sv/virtual_server.rb +79 -9
- data/lib/file_sv/yaml_processor.rb +7 -1
- metadata +32 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb4214e64bca77866967b848fb88c40eabe0a999d03e1ba88e47500662314b44
|
4
|
+
data.tar.gz: de2aa37269d3fc2d22904a1535a2d8e6308f011baae82fd352ee853def74f4d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72685470cd73d2f13d6e0d5894980d7603787204bcd5d454f339f81d105f96f0276b9d32026e3485e02abeb9001fcfe25648d64686fa804c0f57fc5a9df3ec0d
|
7
|
+
data.tar.gz: 786f78f51f7c02605b235c4306b972f67c33cd31c537a1af0ec41b363f5b8c340baee5648f40a3ae9006dc3561783d8dab6086d4412e4844da0515ff5d6175d8
|
data/exe/file_sv
CHANGED
@@ -12,16 +12,24 @@ class Exe < Thor
|
|
12
12
|
ServiceLoader.create_plan_for folder
|
13
13
|
end
|
14
14
|
|
15
|
+
option :crt, default: nil, banner: "HTTPS CRT"
|
16
|
+
option :key, default: nil, banner: "HTTPS key"
|
15
17
|
desc "serve folder", "Serve virtual service based on folder"
|
16
18
|
def serve(folder)
|
17
19
|
plan folder
|
18
|
-
ServiceLoader.serve_plan
|
20
|
+
ServiceLoader.serve_plan options
|
19
21
|
end
|
20
22
|
|
21
|
-
desc "
|
22
|
-
def
|
23
|
+
desc "inspect folder", "Inspect details of what's served at folder"
|
24
|
+
def inspect(folder)
|
23
25
|
require "file_sv"
|
24
|
-
|
26
|
+
ServiceLoader.inspect folder
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "version", "Version of FileSv"
|
30
|
+
def version
|
31
|
+
require "file_sv/version"
|
32
|
+
puts "FileSv version #{FileSv::VERSION}"
|
25
33
|
end
|
26
34
|
end
|
27
35
|
|
data/lib/file_sv.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "yaml"
|
3
4
|
require_relative "file_sv/version"
|
4
5
|
require_relative "file_sv/global_settings"
|
5
6
|
require_relative "file_sv/sv_plan"
|
@@ -15,29 +16,37 @@ module FileSv
|
|
15
16
|
class FileNameError < Error; end
|
16
17
|
|
17
18
|
class << self
|
19
|
+
# @return [Hash] Mapping of REST method names to setting classes
|
18
20
|
def rest_methods
|
19
21
|
{
|
20
22
|
get: GetSettings, post: PostSettings, patch: PatchSettings, options: OptionsSettings,
|
21
|
-
delete: DeleteSettings
|
23
|
+
delete: DeleteSettings, put: PutSettings
|
22
24
|
}
|
23
25
|
end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
|
-
require "yaml"
|
28
29
|
CONFIG_FILE = "file_sv.yaml"
|
29
|
-
if File.exist? CONFIG_FILE
|
30
|
-
# Set values in global settings based on config
|
31
|
-
class GlobalSettings
|
32
|
-
config = YAML.load_file CONFIG_FILE
|
33
|
-
config["global"]&.each do |key, value|
|
34
|
-
send("#{key}=", value)
|
35
|
-
end
|
36
30
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
31
|
+
# Set values in global settings based on config
|
32
|
+
def load_default_config(file_path)
|
33
|
+
return unless File.exist? file_path
|
34
|
+
|
35
|
+
config = YAML.load_file file_path
|
36
|
+
return unless config # Handle empty YAML file
|
37
|
+
|
38
|
+
config["global"]&.each do |key, value|
|
39
|
+
GlobalSettings.send("#{key}=", value)
|
42
40
|
end
|
41
|
+
|
42
|
+
load_rest_method_config config
|
43
43
|
end
|
44
|
+
|
45
|
+
# Load details of each REST method
|
46
|
+
def load_rest_method_config(config)
|
47
|
+
FileSv.rest_methods.each do |method, setting_class|
|
48
|
+
config[method.to_s]&.each { |key, value| setting_class.send("#{key}=", value) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
load_default_config CONFIG_FILE
|
Binary file
|
@@ -5,11 +5,23 @@ class GlobalSettings
|
|
5
5
|
@default_method = "get"
|
6
6
|
|
7
7
|
@empty_body_status = 204
|
8
|
+
|
9
|
+
@ignore_files = "{*.md,Dockerfile,.*}"
|
10
|
+
|
11
|
+
@https = false
|
8
12
|
class << self
|
9
13
|
# @return [String] Default REST method when none specified by filename
|
10
14
|
attr_accessor :default_method
|
11
15
|
# @return [Integer] Default status of response when file is empty
|
12
16
|
attr_accessor :empty_body_status
|
17
|
+
# @return [Array] Expression representing files to ignore
|
18
|
+
attr_accessor :ignore_files
|
19
|
+
# @return [Boolean] Whether to serve https using self signed certificate
|
20
|
+
attr_accessor :https
|
21
|
+
# @return [String] Path to HTTPS cert
|
22
|
+
attr_accessor :cert
|
23
|
+
# @return [String] Path to HTTPS key
|
24
|
+
attr_accessor :key
|
13
25
|
end
|
14
26
|
end
|
15
27
|
|
@@ -19,25 +31,36 @@ end
|
|
19
31
|
|
20
32
|
# Settings specific to GET
|
21
33
|
class GetSettings
|
34
|
+
@default_status = 200
|
22
35
|
extend CommonHttpSettings
|
23
36
|
end
|
24
37
|
|
25
38
|
# Settings specific to POST
|
26
39
|
class PostSettings
|
40
|
+
@default_status = 201
|
27
41
|
extend CommonHttpSettings
|
28
42
|
end
|
29
43
|
|
30
44
|
# Settings specific to PATCH
|
31
45
|
class PatchSettings
|
46
|
+
@default_status = 200
|
47
|
+
extend CommonHttpSettings
|
48
|
+
end
|
49
|
+
|
50
|
+
# Settings specific to PATCH
|
51
|
+
class PutSettings
|
52
|
+
@default_status = 200
|
32
53
|
extend CommonHttpSettings
|
33
54
|
end
|
34
55
|
|
35
56
|
# Settings specific to OPTIONS
|
36
57
|
class OptionsSettings
|
58
|
+
@default_status = 200
|
37
59
|
extend CommonHttpSettings
|
38
60
|
end
|
39
61
|
|
40
62
|
# Settings specific to DELETE
|
41
63
|
class DeleteSettings
|
64
|
+
@default_status = 200
|
42
65
|
extend CommonHttpSettings
|
43
66
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require "erb"
|
4
4
|
require "securerandom"
|
5
|
+
require "faker"
|
6
|
+
require "pathname"
|
5
7
|
|
6
8
|
# Endpoint planned to be served
|
7
9
|
class PlannedEndpoint
|
@@ -15,12 +17,12 @@ class PlannedEndpoint
|
|
15
17
|
attr_accessor :status_code
|
16
18
|
|
17
19
|
# @return [Array]
|
18
|
-
HTTP_METHODS = %w[get post patch delete options].freeze
|
20
|
+
HTTP_METHODS = %w[get put post patch delete options].freeze
|
19
21
|
|
20
22
|
# Represent a new endpoint
|
21
23
|
def initialize(path)
|
22
24
|
self.file_path = path
|
23
|
-
self.path =
|
25
|
+
self.path = serving_loc_for path
|
24
26
|
@file = true if not_text?
|
25
27
|
assign_params_from_name
|
26
28
|
self.method ||= GlobalSettings.default_method
|
@@ -29,21 +31,28 @@ class PlannedEndpoint
|
|
29
31
|
set_default_status_code
|
30
32
|
end
|
31
33
|
|
34
|
+
def serving_loc_for(path)
|
35
|
+
loc = File.split(path).first # TODO: Handle if path has a % at beginning of a folder
|
36
|
+
loc.gsub("#{File::SEPARATOR}%", "#{File::SEPARATOR}:")
|
37
|
+
end
|
38
|
+
|
32
39
|
# @return [Boolean] Whether endpoint serves a file not a string
|
33
40
|
def file?
|
34
41
|
@file
|
35
42
|
end
|
36
43
|
|
44
|
+
# Return default status code for an empty body
|
37
45
|
def default_empty_code
|
38
46
|
return false if not_text?
|
39
47
|
|
40
|
-
if content.strip.empty?
|
48
|
+
if content(binding).strip.empty?
|
41
49
|
self.status_code = GlobalSettings.empty_body_status
|
42
50
|
return true
|
43
51
|
end
|
44
52
|
false
|
45
53
|
end
|
46
54
|
|
55
|
+
# Set default status code based on empty body or type of METHOD
|
47
56
|
def set_default_status_code
|
48
57
|
return if default_empty_code
|
49
58
|
|
@@ -90,8 +99,8 @@ class PlannedEndpoint
|
|
90
99
|
end
|
91
100
|
|
92
101
|
# @return [Object] Content of file
|
93
|
-
def content
|
94
|
-
render_text
|
102
|
+
def content(binding)
|
103
|
+
render_text(binding)
|
95
104
|
end
|
96
105
|
|
97
106
|
def not_text?
|
@@ -100,7 +109,7 @@ class PlannedEndpoint
|
|
100
109
|
end
|
101
110
|
|
102
111
|
# @return [String] Render text
|
103
|
-
def render_text
|
112
|
+
def render_text(binding)
|
104
113
|
ERB.new(File.read(serving_file_name)).result(binding)
|
105
114
|
end
|
106
115
|
end
|
@@ -9,13 +9,21 @@ module ServiceLoader
|
|
9
9
|
# Create virtual service plan based on folder
|
10
10
|
def create_plan_for(folder)
|
11
11
|
SvPlan.create folder
|
12
|
+
puts SvPlan.show
|
13
|
+
end
|
14
|
+
|
15
|
+
# Inspect plan
|
16
|
+
def inspect(folder)
|
17
|
+
create_plan_for folder
|
12
18
|
puts SvPlan.inspect
|
13
19
|
end
|
14
20
|
|
15
21
|
# Serve plan
|
16
|
-
def serve_plan
|
22
|
+
def serve_plan(thor_options)
|
17
23
|
require "sinatra"
|
18
24
|
require_relative "virtual_server"
|
25
|
+
GlobalSettings.key = thor_options[:key] if thor_options[:key]
|
26
|
+
GlobalSettings.cert = thor_options[:crt] if thor_options[:crt]
|
19
27
|
VirtualServer.run!
|
20
28
|
end
|
21
29
|
end
|
data/lib/file_sv/sv_plan.rb
CHANGED
@@ -6,7 +6,7 @@ require_relative "file_processor"
|
|
6
6
|
class SvPlan
|
7
7
|
@endpoints = {}
|
8
8
|
class << self
|
9
|
-
# @return [
|
9
|
+
# @return [Hash] Endpoints included in plan. Key - endpoint, value - methods served under it
|
10
10
|
attr_reader :endpoints
|
11
11
|
# @return [String] Folder plan is served from
|
12
12
|
attr_accessor :serving_folder
|
@@ -15,33 +15,55 @@ class SvPlan
|
|
15
15
|
def create(folder)
|
16
16
|
self.serving_folder = folder
|
17
17
|
puts "Creating service based on files in #{folder}"
|
18
|
-
file_list = Dir.glob("#{folder}/**/*.*")
|
19
|
-
file_list.each
|
20
|
-
process_file file
|
21
|
-
end
|
18
|
+
file_list = Dir.glob("#{folder}/**/*.*") - Dir.glob("#{folder}/#{GlobalSettings.ignore_files}")
|
19
|
+
file_list.each { |file| process_file file }
|
22
20
|
end
|
23
21
|
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
# Process file, for the most part creating endpoint.method from it
|
23
|
+
# @param [String] filename Path to file to process
|
24
|
+
def process_file(filename)
|
25
|
+
filename.slice! serving_folder
|
26
|
+
extension = File.extname(filename)
|
27
27
|
case extension
|
28
|
-
when "yaml" then YamlProcessor.process(
|
28
|
+
when ".yaml" then YamlProcessor.process(filename)
|
29
29
|
else
|
30
|
-
FileProcessor.process(
|
30
|
+
FileProcessor.process(filename)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
|
34
|
+
# Show plan
|
35
|
+
def show
|
36
|
+
endpoint_desc = ""
|
37
|
+
endpoints.sort { |a, b| a[0].length - b[0].length }.each do |endpoint, methods|
|
38
|
+
endpoint_desc += "#{endpoint} \n"
|
39
|
+
methods.each do |method_name, endpoints|
|
40
|
+
endpoint_desc += description_message method_name, endpoints
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
35
44
|
"** VIRTUAL SERVICE PLAN **
|
36
|
-
Serving based on folder: #{serving_folder}
|
37
|
-
|
38
|
-
|
45
|
+
Serving based on folder: #{Dir.pwd}. Related to folder executed: #{serving_folder}
|
46
|
+
#{endpoint_desc}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Inspect details
|
50
|
+
def inspect
|
51
|
+
"Endpoints: #{endpoints.inspect}"
|
39
52
|
end
|
40
53
|
|
54
|
+
# Add endpoint to plan
|
55
|
+
# @param [PlannedEndpoint] other Endpoint to add to plan
|
41
56
|
def +(other)
|
42
57
|
@endpoints[other.path] ||= {}
|
43
58
|
@endpoints[other.path][other.method] ||= []
|
44
59
|
@endpoints[other.path][other.method] << other
|
45
60
|
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @return [String]
|
65
|
+
def description_message(method_name, endpoints)
|
66
|
+
" #{method_name.upcase} (#{endpoints.size} responses) #{endpoints.collect(&:status_code)}\n"
|
67
|
+
end
|
46
68
|
end
|
47
69
|
end
|
data/lib/file_sv/version.rb
CHANGED
@@ -2,27 +2,97 @@
|
|
2
2
|
|
3
3
|
require "webrick"
|
4
4
|
require "sinatra"
|
5
|
+
require "docdsl"
|
6
|
+
require "webrick/https"
|
7
|
+
require "openssl"
|
5
8
|
|
6
9
|
# Virtual server hosting virtual service defined through files
|
7
10
|
class VirtualServer < Sinatra::Base
|
8
11
|
set :server, "webrick"
|
9
12
|
set :bind, "0.0.0.0"
|
10
13
|
|
14
|
+
register Sinatra::DocDsl
|
15
|
+
|
16
|
+
if GlobalSettings.https
|
17
|
+
def self.own_certs(webrick_options)
|
18
|
+
puts "Using cert from #{GlobalSettings.cert}"
|
19
|
+
cert = OpenSSL::X509::Certificate.new File.read GlobalSettings.cert
|
20
|
+
pkey = OpenSSL::PKey::RSA.new File.read GlobalSettings.key
|
21
|
+
webrick_options[:SSLCertificate] = cert
|
22
|
+
webrick_options[:SSLPrivateKey] = pkey
|
23
|
+
webrick_options
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.determine_certs(webrick_options)
|
27
|
+
if GlobalSettings.key && GlobalSettings.cert
|
28
|
+
webrick_options = own_certs webrick_options
|
29
|
+
else
|
30
|
+
puts "Using self signed cert"
|
31
|
+
webrick_options[:ServerName] = "localhost"
|
32
|
+
webrick_options[:SSLCertName] = "/CN=localhost"
|
33
|
+
end
|
34
|
+
webrick_options
|
35
|
+
end
|
36
|
+
|
37
|
+
# Run as https with self signed cert
|
38
|
+
def self.run!
|
39
|
+
logger = WEBrick::Log.new(nil, WEBrick::BasicLog::WARN)
|
40
|
+
webrick_options = { Port: port, SSLEnable: true, Logger: logger }
|
41
|
+
webrick_options = determine_certs webrick_options
|
42
|
+
# TODO: Following run does not work on Ruby 3
|
43
|
+
Rack::Handler::WEBrick.run(self, webrick_options) do |server|
|
44
|
+
%i[INT TERM].each { |sig| trap(sig) { server.stop } }
|
45
|
+
server.threaded = settings.threaded if server.respond_to? :threaded=
|
46
|
+
set :running, true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
page do
|
52
|
+
title "File SV"
|
53
|
+
header "Service virtualization created from #{Dir.pwd}"
|
54
|
+
introduction 'Created using file_sv. See <a href="https://gitlab.com/samuel-garratt/file_sv">File SV</a>
|
55
|
+
for more details'
|
56
|
+
end
|
57
|
+
|
58
|
+
get "/favicon.ico" do
|
59
|
+
send_file File.join(__dir__, "file_sv.ico")
|
60
|
+
end
|
61
|
+
|
62
|
+
doc_endpoint "/docs"
|
63
|
+
|
64
|
+
# Output for endpoint, either a file or text content
|
65
|
+
# @param [PlannedEndpoint] endpoint Planned endpoint to serve
|
66
|
+
def output_for(endpoint, binding)
|
67
|
+
endpoint.file? ? send_file(endpoint.serving_file_name) : endpoint.content(binding)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Log endpoint. Return content and status code defined by endpoint
|
71
|
+
# @param [PlannedEndpoint] endpoint Planned endpoint to serve
|
72
|
+
def serve(endpoint, id = nil)
|
73
|
+
message = "Using endpoint based on file #{endpoint.serving_file_name}."
|
74
|
+
@id = id
|
75
|
+
message += " Using param '#{@id}'" if id
|
76
|
+
puts message
|
77
|
+
[endpoint.status_code, output_for(endpoint, binding)]
|
78
|
+
end
|
79
|
+
|
11
80
|
SvPlan.endpoints.each do |_endpoint_path, endpoint_value|
|
12
81
|
endpoint_value.each do |_method_name, endpoints|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
82
|
+
endpoint_base = endpoints[0]
|
83
|
+
documentation "Endpoint #{endpoint_base.path}" do
|
84
|
+
response "#{endpoints.size} kinds of response"
|
85
|
+
end
|
86
|
+
if endpoint_base.path.include? "#{File::Separator}:"
|
87
|
+
send(endpoint_base.method, endpoint_base.path) do |id|
|
88
|
+
endpoint = endpoints.sample
|
89
|
+
serve endpoint, id
|
17
90
|
end
|
18
91
|
else
|
19
|
-
endpoint_base = endpoints[0]
|
20
92
|
send(endpoint_base.method, endpoint_base.path) do
|
21
|
-
|
22
|
-
endpoint
|
23
|
-
[endpoint.status_code, endpoint.file? ? send_file(endpoint.serving_file_name) : endpoint.content]
|
93
|
+
endpoint = endpoints.sample
|
94
|
+
serve endpoint
|
24
95
|
end
|
25
|
-
# Average same methods at same endpoint
|
26
96
|
end
|
27
97
|
end
|
28
98
|
end
|
@@ -3,8 +3,14 @@
|
|
3
3
|
# Process YAML files
|
4
4
|
class YamlProcessor
|
5
5
|
class << self
|
6
|
+
# Process YAML file
|
6
7
|
def process(filename)
|
7
|
-
|
8
|
+
if filename == "/file_sv.yaml"
|
9
|
+
puts "Overriding default config based on #{File.join(Dir.pwd, filename[1..-1])}"
|
10
|
+
load_default_config filename[1..-1]
|
11
|
+
else
|
12
|
+
puts "Skipping #{filename}"
|
13
|
+
end
|
8
14
|
end
|
9
15
|
end
|
10
16
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: file_sv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Garratt
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faker
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: sinatra
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -24,6 +38,20 @@ dependencies:
|
|
24
38
|
- - ">="
|
25
39
|
- !ruby/object:Gem::Version
|
26
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sinatra-docdsl
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: thor
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -65,6 +93,7 @@ files:
|
|
65
93
|
- exe/file_sv
|
66
94
|
- lib/file_sv.rb
|
67
95
|
- lib/file_sv/file_processor.rb
|
96
|
+
- lib/file_sv/file_sv.ico
|
68
97
|
- lib/file_sv/global_settings.rb
|
69
98
|
- lib/file_sv/planned_endpoint.rb
|
70
99
|
- lib/file_sv/render_file.rb
|
@@ -95,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
124
|
- !ruby/object:Gem::Version
|
96
125
|
version: '0'
|
97
126
|
requirements: []
|
98
|
-
rubygems_version: 3.
|
127
|
+
rubygems_version: 3.1.4
|
99
128
|
signing_key:
|
100
129
|
specification_version: 4
|
101
130
|
summary: REST service virtualisation through file structure.
|