simple-httpd 0.0.4 → 0.3.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop.yml +9 -0
  4. data/.tm_properties +1 -0
  5. data/Gemfile +21 -1
  6. data/Makefile +9 -0
  7. data/README.md +87 -2
  8. data/Rakefile +5 -0
  9. data/VERSION +1 -1
  10. data/bin/simple-httpd +13 -0
  11. data/examples/README.md +41 -0
  12. data/examples/ex1/ex1_helpers.rb +5 -0
  13. data/examples/ex1/root.rb +11 -0
  14. data/examples/ex2/README.txt +1 -0
  15. data/examples/ex2/ex2_helpers.rb +5 -0
  16. data/examples/ex2/helpers.rb +15 -0
  17. data/examples/ex2/info.rb +4 -0
  18. data/examples/ex2/root.rb +3 -0
  19. data/examples/ex3/example_service.rb +13 -0
  20. data/examples/services/example_service.rb +25 -0
  21. data/examples/services/explicit_example_service.rb +18 -0
  22. data/examples/v2/api.js +1 -0
  23. data/examples/v2/jobs.rb +13 -0
  24. data/examples/v2/root.rb +3 -0
  25. data/examples/v2/v2_helpers.rb +5 -0
  26. data/lib/simple-service.rb +3 -0
  27. data/lib/simple/httpd.rb +99 -25
  28. data/lib/simple/httpd/base_controller.rb +2 -2
  29. data/lib/simple/httpd/base_controller/error_handling.rb +45 -17
  30. data/lib/simple/httpd/base_controller/json.rb +15 -8
  31. data/lib/simple/httpd/cli.rb +99 -0
  32. data/lib/simple/httpd/helpers.rb +54 -0
  33. data/lib/simple/httpd/mount_spec.rb +106 -0
  34. data/lib/simple/httpd/rack.rb +17 -0
  35. data/lib/simple/httpd/rack/dynamic_mount.rb +66 -0
  36. data/lib/simple/httpd/rack/merger.rb +28 -0
  37. data/lib/simple/httpd/rack/static_mount.rb +50 -0
  38. data/lib/simple/httpd/server.rb +69 -0
  39. data/lib/simple/httpd/service.rb +70 -0
  40. data/lib/simple/httpd/version.rb +1 -1
  41. data/lib/simple/service.rb +69 -0
  42. data/lib/simple/service/action.rb +78 -0
  43. data/lib/simple/service/context.rb +46 -0
  44. data/scripts/release +2 -0
  45. data/scripts/release.rb +91 -0
  46. data/simple-httpd.gemspec +9 -19
  47. data/spec/simple/httpd/base_controller/httpd_cors_spec.rb +15 -0
  48. data/spec/simple/httpd/base_controller/httpd_debug_spec.rb +11 -0
  49. data/spec/simple/httpd/base_controller/httpd_x_processing_copy.rb +15 -0
  50. data/spec/simple/httpd/base_spec.rb +16 -0
  51. data/spec/simple/httpd/dynamic_mounting_spec.rb +33 -0
  52. data/spec/simple/httpd/helpers_spec.rb +15 -0
  53. data/spec/simple/httpd/rspec_httpd_spec.rb +17 -0
  54. data/spec/simple/httpd/services/service_explicit_spec.rb +34 -0
  55. data/spec/simple/httpd/services/service_spec.rb +34 -0
  56. data/spec/simple/httpd/static_mounting_spec.rb +13 -0
  57. data/spec/spec_helper.rb +30 -6
  58. data/spec/support/004_simplecov.rb +3 -12
  59. metadata +61 -84
  60. data/lib/simple/httpd/app.rb +0 -84
  61. data/lib/simple/httpd/app/file_server.rb +0 -19
  62. data/spec/simple/httpd/version_spec.rb +0 -10
  63. data/tasks/release.rake +0 -104
@@ -0,0 +1,28 @@
1
+ # A simple file server middleware
2
+ class Simple::Httpd::Rack::Merger
3
+ Rack = Simple::Httpd::Rack
4
+
5
+ # returns an app that merges other apps
6
+ def self.build(apps)
7
+ return apps.first if apps.length == 1
8
+
9
+ new(apps)
10
+ end
11
+
12
+ private
13
+
14
+ def initialize(apps)
15
+ @apps = apps
16
+ end
17
+
18
+ public
19
+
20
+ def call(env)
21
+ @apps.each do |app|
22
+ status, body, headers = app.call(env)
23
+ return [status, body, headers] unless status == 404
24
+ end
25
+
26
+ Rack.error 404, "No such action"
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ # A simple file server middleware
2
+ class Simple::Httpd::Rack::StaticMount
3
+ Rack = ::Simple::Httpd::Rack
4
+
5
+ EXTENSIONS = %w(.txt .md .js .css .png .jpeg .jpg)
6
+
7
+ def self.build(mount_point, path)
8
+ static_files = static_files(path)
9
+ return nil if static_files.empty?
10
+
11
+ ::Simple::Httpd.logger.info do
12
+ "#{mount_point}: serving #{static_files.count} static file(s)"
13
+ end
14
+
15
+ new(path, static_files)
16
+ end
17
+
18
+ def self.static_files(path)
19
+ Dir.chdir(path) do
20
+ pattern = "**/*{" + EXTENSIONS.join(",") + "}"
21
+ Dir.glob(pattern)
22
+ end
23
+ end
24
+
25
+ attr_reader :mount_point, :path
26
+
27
+ def initialize(path, static_files)
28
+ @path = path
29
+ @static_files = Set.new(static_files)
30
+ end
31
+
32
+ def call(env)
33
+ request_path = env["PATH_INFO"]
34
+ if serve_file?(request_path)
35
+ file_path = request_path[1..-1]
36
+ env["PATH_INFO"] = file_path
37
+ file_server.call(env)
38
+ else
39
+ Rack.error 404, "No such file"
40
+ end
41
+ end
42
+
43
+ def file_server
44
+ @file_server ||= ::Rack::File.new(path)
45
+ end
46
+
47
+ def serve_file?(request_path)
48
+ @static_files.include?(request_path[1..-1])
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ class Simple::Httpd
2
+ module Server
3
+ extend self
4
+
5
+ module NullLogger # :nodoc:
6
+ extend self
7
+
8
+ def <<(msg); end
9
+ end
10
+
11
+ def listen!(app, environment: "development", host: nil, port:, logger: nil)
12
+ expect! app != nil
13
+
14
+ host ||= "127.0.0.1"
15
+ URI("http://#{host}:#{port}") # validate host and port
16
+
17
+ logger ||= ::Simple::Httpd.logger
18
+
19
+ prepare_logger!(logger)
20
+ logger.info "Starting httpd server on http://#{host}:#{port}/"
21
+
22
+ app = ::Rack::Lint.new(app) if environment != "production"
23
+
24
+ # re/AccessLog: the AccessLog setting points WEBrick's access logging to the
25
+ # NullLogger object.
26
+ #
27
+ # Instead we'll use a combination of Rack::CommonLogger (see Simple::Httpd.app),
28
+ # and sinatra's logger (see Simple::Httpd::BaseController).
29
+ ::Rack::Server.start app: app,
30
+ Host: host,
31
+ Port: port,
32
+ environment: environment,
33
+ Logger: logger,
34
+ AccessLog: [[NullLogger, ""]]
35
+ end
36
+
37
+ private
38
+
39
+ # When Webrick is being shut down via SIGTERM - which we do at least during
40
+ # rspec-httpd triggered runs - it sends a fatal message to the logger. We catch
41
+ # it - to "downgrade" it to INFO - but we still abort.
42
+ def prepare_logger!(logger)
43
+ def logger.fatal(msg, &block)
44
+ if msg.is_a?(SignalException) && msg.signo == ::Signal.list["TERM"]
45
+ if %w(test development).include?(::Simple::Httpd.env)
46
+ info "Received SIGTERM: hard killing server (due to running in #{::Simple::Httpd.env.inspect} environment)"
47
+ Simple::Httpd::Server.exit!
48
+ else
49
+ info "Received SIGTERM: shutting down server..."
50
+ exit 1
51
+ end
52
+ end
53
+
54
+ super
55
+ end
56
+ end
57
+
58
+ public
59
+
60
+ def exit!(exit_status = 1)
61
+ # Run SimpleCov if exists, and if this is the PID that started SimpleCov in the first place.
62
+ if defined?(SimpleCov) && SimpleCov.pid == Process.pid
63
+ SimpleCov.process_result(SimpleCov.result, 0)
64
+ end
65
+
66
+ Kernel.exit! exit_status
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,70 @@
1
+ require "simple-service"
2
+
3
+ class Simple::Httpd::Service
4
+ module ControllerAdapter
5
+ def mount_service(service)
6
+ @service = service
7
+
8
+ instance_eval do
9
+ yield(service)
10
+ end
11
+ ensure
12
+ @service = nil
13
+ end
14
+
15
+ def get(path, opts = {}, &block)
16
+ service_route?("GET", path, opts, &block) || super
17
+ end
18
+
19
+ def post(path, opts = {}, &block)
20
+ service_route?("POST", path, opts, &block) || super
21
+ end
22
+
23
+ def put(path, opts = {}, &block)
24
+ service_route?("PUT", path, opts, &block) || super
25
+ end
26
+
27
+ def delete(path, opts = {}, &block)
28
+ service_route?("DELETE", path, opts, &block) || super
29
+ end
30
+
31
+ def head(path, opts = {}, &block)
32
+ service_route?("HEAD", path, opts, &block) || super
33
+ end
34
+
35
+ private
36
+
37
+ def service_route?(verb, path, opts, &block)
38
+ return false unless @service
39
+ return false if block
40
+ return false unless opts.empty?
41
+ return false unless path.is_a?(Hash) && path.size == 1
42
+
43
+ path, action_name = *path.first
44
+
45
+ # Verify existence of this action.
46
+ @service.fetch_action(action_name)
47
+
48
+ # get service reference into binding, to make it available for the route
49
+ # definition.
50
+ service = @service
51
+
52
+ # define sinatra route.
53
+ route(verb, path) do
54
+ result = service.call(action_name, parsed_body, params, context: context)
55
+ json(result)
56
+ end
57
+
58
+ true
59
+ end
60
+ end
61
+
62
+ module Helpers
63
+ def context
64
+ @context ||= ::Simple::Service::Context.new
65
+ end
66
+ end
67
+
68
+ ::Simple::Httpd::BaseController.extend(ControllerAdapter)
69
+ ::Simple::Httpd::BaseController.helpers(Helpers)
70
+ end
@@ -1,4 +1,4 @@
1
- module Simple::Httpd
1
+ class Simple::Httpd
2
2
  module GemHelper
3
3
  extend self
4
4
 
@@ -0,0 +1,69 @@
1
+ module Simple::Service
2
+ class ArgumentError < ::ArgumentError
3
+ end
4
+ end
5
+
6
+ require_relative "service/action"
7
+ require_relative "service/context"
8
+
9
+ module Simple::Service
10
+ def self.included(klass)
11
+ klass.extend ClassMethods
12
+ end
13
+
14
+ def self.context
15
+ Thread.current[:"Simple::Service.context"]
16
+ end
17
+
18
+ def self.with_context(ctx)
19
+ old_ctx = Thread.current[:"Simple::Service.context"]
20
+ Thread.current[:"Simple::Service.context"] = ctx
21
+ yield
22
+ ensure
23
+ Thread.current[:"Simple::Service.context"] = old_ctx
24
+ end
25
+
26
+ module ClassMethods
27
+ def actions
28
+ @actions ||= Action.build_all(service_module: self)
29
+ end
30
+
31
+ def build_service_instance
32
+ service_instance = Object.new
33
+ service_instance.extend self
34
+ service_instance
35
+ end
36
+
37
+ def fetch_action(action_name)
38
+ actions.fetch(action_name) do
39
+ informal = "service #{self} has these actions: #{actions.keys.sort.map(&:inspect).join(", ")}"
40
+ raise "No such action #{action_name.inspect}; #{informal}"
41
+ end
42
+ end
43
+
44
+ def call(action_name, arguments, params, context: nil)
45
+ ::Simple::Service.with_context(context) do
46
+ fetch_action(action_name).invoke(arguments, params)
47
+ end
48
+ end
49
+ end
50
+
51
+ # Resolves a service by name. Returns nil if the name does not refer to a service,
52
+ # or the service module otherwise.
53
+ def self.resolve(str)
54
+ return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
55
+
56
+ service = resolve_constant(str)
57
+
58
+ return unless service.is_a?(Module)
59
+ return unless service.include?(::Simple::Service)
60
+
61
+ service
62
+ end
63
+
64
+ def self.resolve_constant(str)
65
+ const_get(str)
66
+ rescue NameError
67
+ nil
68
+ end
69
+ end
@@ -0,0 +1,78 @@
1
+ module Simple::Service
2
+ class Action
3
+ ArgumentError = ::Simple::Service::ArgumentError
4
+
5
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*"
6
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z")
7
+
8
+ def self.build_all(service_module:)
9
+ service_module.public_instance_methods(false)
10
+ .grep(IDENTIFIER_REGEXP)
11
+ .inject({}) { |hsh, name| hsh.update name => Action.new(service_module, name) }
12
+ end
13
+
14
+ attr_reader :service
15
+ attr_reader :name
16
+ attr_reader :arguments
17
+ attr_reader :parameters
18
+
19
+ def initialize(service, name)
20
+ instance_method = service.instance_method(name)
21
+
22
+ @service = service
23
+ @name = name
24
+ @arguments = []
25
+ @parameters = []
26
+
27
+ instance_method.parameters.each do |kind, parameter_name|
28
+ case kind
29
+ when :req, :opt then @arguments << parameter_name
30
+ when :keyreq, :key then @parameters << parameter_name
31
+ else
32
+ raise ArgumentError, "#{full_name}: no support for #{kind.inspect} arguments, w/parameter #{parameter_name}"
33
+ end
34
+ end
35
+ end
36
+
37
+ # build a service_instance and run the action, with arguments constructed from
38
+ # args_hsh and params_hsh
39
+ def invoke(args_hsh, params_hsh)
40
+ args_hsh ||= {}
41
+ params_hsh ||= {}
42
+
43
+ # build arguments array
44
+ args = extract_arguments(args_hsh)
45
+ args << extract_parameters(params_hsh) unless parameters.empty?
46
+
47
+ # run the action. Note: public_send is only
48
+ # an extra safeguard; since actions are already built off public methods
49
+ # there should be no way to call a private service method.
50
+ service_instance = service.build_service_instance
51
+ service_instance.public_send(@name, *args)
52
+ end
53
+
54
+ private
55
+
56
+ def extract_arguments(args_hsh)
57
+ arguments.map do |name|
58
+ args_hsh.fetch(name.to_s) { raise ArgumentError, "Missing argument in request body: #{name}" }
59
+ end
60
+ end
61
+
62
+ def extract_parameters(params_hsh)
63
+ # Note: in contrast to arguments that are being read from the body parameters that
64
+ # are not submitted are being ignored (and filled in by +nil+).
65
+ #
66
+ # Note 2: The parameter names **must** be Symbols, not Strings, otherwise
67
+ # the service_instance.send invocation later would not fill in keyword
68
+ # arguments from the parameters hash.
69
+ parameters.inject({}) do |hsh, parameter|
70
+ hsh.update parameter => params_hsh.fetch(parameter, nil)
71
+ end
72
+ end
73
+
74
+ def full_name
75
+ "#{service}##{name}"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,46 @@
1
+ module Simple::Service
2
+ class Context
3
+ def initialize
4
+ @hsh = {}
5
+ end
6
+
7
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*"
8
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z")
9
+ ASSIGNMENT_REGEXP = Regexp.compile("\\A(#{IDENTIFIER_PATTERN})=\\z")
10
+
11
+ def [](key)
12
+ key = key.to_sym
13
+ @hsh[key]
14
+ end
15
+
16
+ def []=(key, value)
17
+ key = key.to_sym
18
+ existing_value = @hsh[key]
19
+
20
+ unless existing_value.nil? || existing_value == value
21
+ raise "Cannot overwrite existing context setting #{key.inspect}"
22
+ end
23
+
24
+ @hsh[key] = value
25
+ end
26
+
27
+ def method_missing(sym, *args, &block)
28
+ if block
29
+ super
30
+ elsif args.count == 0 && sym =~ IDENTIFIER_REGEXP
31
+ self[sym]
32
+ elsif args.count == 1 && sym =~ ASSIGNMENT_REGEXP
33
+ self[$1.to_sym] = args.first
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(sym, include_private = false)
40
+ return true if IDENTIFIER_REGEXP.maptch?(sym)
41
+ return true if ASSIGNMENT_REGEXP.maptch?(sym)
42
+
43
+ super
44
+ end
45
+ end
46
+ end
data/scripts/release ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ $0.rb "$@"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # -- helpers ------------------------------------------------------------------
4
+
5
+ def sys(cmd)
6
+ STDERR.puts "> #{cmd}"
7
+ system cmd
8
+ return true if $?.success?
9
+
10
+ STDERR.puts "> #{cmd} returned with exitstatus #{$?.exitstatus}"
11
+ $?.success?
12
+ end
13
+
14
+ def sys!(cmd, error: nil)
15
+ return true if sys(cmd)
16
+ STDERR.puts error if error
17
+ exit 1
18
+ end
19
+
20
+ def die!(msg)
21
+ STDERR.puts msg
22
+ exit 1
23
+ end
24
+
25
+ ROOT = File.expand_path("#{File.dirname(__FILE__)}/..")
26
+
27
+ GEMSPEC = Dir.glob("*.gemspec").first || die!("Missing gemspec file.")
28
+
29
+ # -- Version reading and bumping ----------------------------------------------
30
+
31
+ module Version
32
+ extend self
33
+
34
+ VERSION_FILE = "#{Dir.getwd}/VERSION"
35
+
36
+ def read_version
37
+ version = File.exist?(VERSION_FILE) ? File.read(VERSION_FILE) : "0.0.1"
38
+ version.chomp!
39
+ raise "Invalid version number in #{VERSION_FILE}" unless version =~ /^\d+\.\d+\.\d+$/
40
+ version
41
+ end
42
+
43
+ def auto_version_bump
44
+ old_version_number = read_version
45
+ old = old_version_number.split('.')
46
+
47
+ current = old[0..-2] << old[-1].next
48
+ current.join('.')
49
+ end
50
+
51
+ def bump_version
52
+ next_version = ENV["VERSION"] || auto_version_bump
53
+ File.open(VERSION_FILE, "w") { |io| io.write next_version }
54
+ end
55
+ end
56
+
57
+ # -- check, bump, release a new gem version -----------------------------------
58
+
59
+ Dir.chdir ROOT
60
+ $BASE_BRANCH = ENV['BRANCH'] || 'master'
61
+
62
+ # ENV["BUNDLE_GEMFILE"] = "#{Dir.getwd}/Gemfile"
63
+ # sys! "bundle install"
64
+
65
+ sys! "git diff --exit-code > /dev/null", error: 'There are unstaged changes in your working directory'
66
+ sys! "git diff --cached --exit-code > /dev/null", error: 'There are staged but uncommitted changes'
67
+
68
+ sys! "git checkout #{$BASE_BRANCH}"
69
+ sys! "git pull"
70
+
71
+ Version.bump_version
72
+ version = Version.read_version
73
+
74
+ sys! "git add VERSION"
75
+ sys! "git commit -m \"bump gem to v#{version}\""
76
+ sys! "git tag -a v#{version} -m \"Tag #{version}\""
77
+
78
+ sys! "gem build #{GEMSPEC}"
79
+
80
+ sys! "git push origin #{$BASE_BRANCH}"
81
+ sys! 'git push --tags --force'
82
+ sys! "gem push #{Dir.glob('*.gem').first}"
83
+
84
+ sys! "mkdir -p pkg"
85
+ sys! "mv *.gem pkg"
86
+
87
+ STDERR.puts <<-MSG
88
+ ================================================================================
89
+ Thank you for releasing a new gem version. You made my day.
90
+ ================================================================================
91
+ MSG