file_sv 0.1.1 → 0.1.6

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 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.