file_sv 0.1.1 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac55a5c60dd473f97a05d4a7679772db4eaf54d2990420652e2ae96260dc7ec5
4
- data.tar.gz: bf4e58836d690c6058392323144be3c4bbf53143e5535d7b0aa720d51b33462a
3
+ metadata.gz: a6fe6c5fff5f5d93506024349027d89d5ae0331b22f1dc15c94aaf72b18d1c8a
4
+ data.tar.gz: b8c7f47b3602949beb64b5b7e9fa7f859b2fc3020f5c1a3d91fa9a6309a1ad8a
5
5
  SHA512:
6
- metadata.gz: 87ed6a07aad15e535cc11299447538ebdb2683b608b87777ac39a71e8f32c53b99a26d6416d1d5c3c3f5ab8917dd6ec31f511220a218d110514305cf674c20ae
7
- data.tar.gz: 6a3521c2f94a59fc598d2c13c11bacfbc87a3f0213b2d078a6e222db632d80253c9c72d73fc1d2e20f06e6797cc1c612442928fa0e5ed39392601c8caf3fcad6
6
+ metadata.gz: 3b6521770efbd56f4b790650de3801668e80b4c197e995d3cdd1a97326013c58e241d265caf1735eeeae020050aa775ad44d7acb29b0c651aa3401b109bd7be0
7
+ data.tar.gz: ac3d6a6d948161f2b96f38090a148e3605927a9a43a690a05933b3a942905903f1c95b9e84ead1a204441b7edd66ef179be8aac3e30d6ddb2946a0330314d195
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 "version", "Version of FileTest"
22
- def version
23
+ desc "inspect folder", "Inspect details of what's served at folder"
24
+ def inspect(folder)
23
25
  require "file_sv"
24
- puts "GenericTest version #{FileTest::VERSION}"
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,49 @@ 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
- FileSv.rest_methods.each do |method, setting_class|
38
- config[method.to_s]&.each do |key, value|
39
- setting_class.send("#{key}=", value)
40
- end
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)
40
+ end
41
+
42
+ load_rest_method_config config
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
+ # Set global params based on ENV vars
53
+ def set_based_on_env_vars
54
+ GlobalSettings.instance_variables.each do |setting|
55
+ setting_name = setting.to_s[1..-1]
56
+ if ENV[setting_name]
57
+ puts "Setting #{setting_name} to #{ENV[setting_name]}"
58
+ GlobalSettings.send("#{setting_name}=", ENV[setting_name])
41
59
  end
42
60
  end
43
61
  end
62
+
63
+ load_default_config CONFIG_FILE
64
+ set_based_on_env_vars
@@ -6,7 +6,8 @@ class FileProcessor
6
6
  class << self
7
7
  # Process file, adding it to plan
8
8
  def process(filename)
9
- SvPlan + PlannedEndpoint.new(filename)
9
+ endpoint = PlannedEndpoint.new(filename)
10
+ SvPlan + endpoint unless GlobalSettings.ignored_status? endpoint.status_code
10
11
  end
11
12
  end
12
13
  end
Binary file
@@ -5,14 +5,46 @@ 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
12
+
13
+ @ignore_status_codes = nil
8
14
  class << self
9
15
  # @return [String] Default REST method when none specified by filename
10
16
  attr_accessor :default_method
11
17
  # @return [Integer] Default status of response when file is empty
12
18
  attr_accessor :empty_body_status
19
+ # @return [Array] Expression representing files to ignore
20
+ attr_accessor :ignore_files
21
+ # @return [Boolean] Whether to serve https using self signed certificate
22
+ attr_accessor :https
23
+ # @return [String] Path to HTTPS cert
24
+ attr_accessor :cert
25
+ # @return [String] Path to HTTPS key
26
+ attr_accessor :key
27
+ # @return [Array] List of http status codes to ignore
28
+ attr_accessor :ignore_status_codes
29
+
30
+ # @param [Integer] status_code HTTP status code
31
+ # @return [Boolean] Whether status code is currently ignored
32
+ def ignored_status?(status_code)
33
+ return unless ignore_status_codes
34
+
35
+ ignore_status_codes.split(",").each do |code|
36
+ regex = Regexp.new code.to_s
37
+ result = status_code.to_s[regex]
38
+ next unless result
39
+
40
+ return true unless result.empty?
41
+ end
42
+ false
43
+ end
13
44
  end
14
45
  end
15
46
 
47
+ # Http settings that all HTTP method types inherit
16
48
  module CommonHttpSettings
17
49
  attr_accessor :default_status
18
50
  end
@@ -35,6 +67,12 @@ class PatchSettings
35
67
  extend CommonHttpSettings
36
68
  end
37
69
 
70
+ # Settings specific to PATCH
71
+ class PutSettings
72
+ @default_status = 200
73
+ extend CommonHttpSettings
74
+ end
75
+
38
76
  # Settings specific to OPTIONS
39
77
  class OptionsSettings
40
78
  @default_status = 200
@@ -3,6 +3,7 @@
3
3
  require "erb"
4
4
  require "securerandom"
5
5
  require "faker"
6
+ require "pathname"
6
7
 
7
8
  # Endpoint planned to be served
8
9
  class PlannedEndpoint
@@ -16,12 +17,12 @@ class PlannedEndpoint
16
17
  attr_accessor :status_code
17
18
 
18
19
  # @return [Array]
19
- HTTP_METHODS = %w[get post patch delete options].freeze
20
+ HTTP_METHODS = %w[get put post patch delete options].freeze
20
21
 
21
22
  # Represent a new endpoint
22
23
  def initialize(path)
23
24
  self.file_path = path
24
- self.path = File.split(path).first
25
+ self.path = serving_loc_for path
25
26
  @file = true if not_text?
26
27
  assign_params_from_name
27
28
  self.method ||= GlobalSettings.default_method
@@ -30,21 +31,28 @@ class PlannedEndpoint
30
31
  set_default_status_code
31
32
  end
32
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
+
33
39
  # @return [Boolean] Whether endpoint serves a file not a string
34
40
  def file?
35
41
  @file
36
42
  end
37
43
 
44
+ # Return default status code for an empty body
38
45
  def default_empty_code
39
46
  return false if not_text?
40
47
 
41
- if content.strip.empty?
48
+ if content(binding).strip.empty?
42
49
  self.status_code = GlobalSettings.empty_body_status
43
50
  return true
44
51
  end
45
52
  false
46
53
  end
47
54
 
55
+ # Set default status code based on empty body or type of METHOD
48
56
  def set_default_status_code
49
57
  return if default_empty_code
50
58
 
@@ -91,8 +99,8 @@ class PlannedEndpoint
91
99
  end
92
100
 
93
101
  # @return [Object] Content of file
94
- def content
95
- render_text
102
+ def content(binding)
103
+ render_text(binding)
96
104
  end
97
105
 
98
106
  def not_text?
@@ -101,7 +109,7 @@ class PlannedEndpoint
101
109
  end
102
110
 
103
111
  # @return [String] Render text
104
- def render_text
112
+ def render_text(binding)
105
113
  ERB.new(File.read(serving_file_name)).result(binding)
106
114
  end
107
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
@@ -6,7 +6,7 @@ require_relative "file_processor"
6
6
  class SvPlan
7
7
  @endpoints = {}
8
8
  class << self
9
- # @return [Array]
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 do |file|
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
- def process_file(file)
25
- file.slice! serving_folder
26
- extension = File.extname(file)
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(file)
28
+ when ".yaml" then YamlProcessor.process(filename)
29
29
  else
30
- FileProcessor.process(file)
30
+ FileProcessor.process(filename)
31
31
  end
32
32
  end
33
33
 
34
- def inspect
34
+ # Show plan
35
+ def show
36
+ endpoint_desc = ""
37
+ endpoints.sort { |a, b| a[0].casecmp b[0] }.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
- Endpoints: #{endpoints.inspect}
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FileSv
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.6"
5
5
  end
@@ -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
- if endpoints.size < 2
14
- endpoint = endpoints[0]
15
- send(endpoint.method, endpoint.path) do
16
- [endpoint.status_code, endpoint.file? ? send_file(endpoint.serving_file_name) : endpoint.content]
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
- my_points = endpoints
22
- endpoint = my_points.sample
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
- puts "Overriding default config based on #{filename}" if filename == "file_sv"
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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: file_sv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.6
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-03 00:00:00.000000000 Z
11
+ date: 2021-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faker
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: thor
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -79,6 +93,7 @@ files:
79
93
  - exe/file_sv
80
94
  - lib/file_sv.rb
81
95
  - lib/file_sv/file_processor.rb
96
+ - lib/file_sv/file_sv.ico
82
97
  - lib/file_sv/global_settings.rb
83
98
  - lib/file_sv/planned_endpoint.rb
84
99
  - lib/file_sv/render_file.rb
@@ -109,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
124
  - !ruby/object:Gem::Version
110
125
  version: '0'
111
126
  requirements: []
112
- rubygems_version: 3.2.3
127
+ rubygems_version: 3.1.4
113
128
  signing_key:
114
129
  specification_version: 4
115
130
  summary: REST service virtualisation through file structure.