simple-httpd 0.0.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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