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
@@ -12,8 +12,8 @@ class Simple::Httpd::BaseController < Sinatra::Base
12
12
  # Rails development mode, but on the other hand it is much faster, and
13
13
  # probably useful 90% of the time.
14
14
  configure :development do
15
- require "sinatra/reloader"
16
- register Sinatra::Reloader
15
+ # require "sinatra/reloader"
16
+ # register Sinatra::Reloader
17
17
  end
18
18
  end
19
19
 
@@ -1,6 +1,10 @@
1
1
  require_relative "./json"
2
2
 
3
+ # rubocop:disable Metrics/ClassLength
4
+
3
5
  class Simple::Httpd::BaseController
6
+ H = ::Simple::Httpd::Helpers
7
+
4
8
  set :show_exceptions, false
5
9
  set :dump_errors, false
6
10
  set :raise_errors, false
@@ -64,27 +68,51 @@ class Simple::Httpd::BaseController
64
68
  description: error_description(exc)
65
69
  end
66
70
 
67
- # class NotAuthorizedError < RuntimeError
68
- # end
69
- #
70
- # error(NotAuthorizedError) do
71
- # render_error e, status: 403,
72
- # title: "Not authorized",
73
- # description: "You don't have necessary powers to access this page."
74
- # end
71
+ error(::Simple::Service::ArgumentError) do |exc|
72
+ render_error exc, status: 422,
73
+ title: "Invalid input #{exc.message}",
74
+ description: exc.message
75
+ end
75
76
 
76
- # class LoginRequiredError < RuntimeError
77
- # end
78
- #
79
- # error(LoginRequiredError) do
80
- # render_error e, status: 404,
81
- # title: "Not found",
82
- # description: "The server failed to recognize affiliation. Please provide a valid Session-Id."
83
- # end
77
+ # -- not authorized.---------------------------------------------------------
78
+
79
+ class NotAuthorizedError < RuntimeError
80
+ end
81
+
82
+ error(NotAuthorizedError) do
83
+ render_error e, status: 403,
84
+ title: "Not authorized",
85
+ description: "You don't have necessary powers to access this page."
86
+ end
87
+
88
+ def not_authorized!(msg)
89
+ raise NotAuthorizedError, msg
90
+ end
91
+
92
+ # -- login required.---------------------------------------------------------
93
+
94
+ class LoginRequiredError < RuntimeError
95
+ end
96
+
97
+ def login_required!(msg)
98
+ raise LoginRequiredError, msg
99
+ end
100
+
101
+ error(LoginRequiredError) do
102
+ render_error e, status: 404,
103
+ title: "Not found",
104
+ description: "The server failed to recognize affiliation. Please provide a valid Session-Id."
105
+ end
106
+
107
+ # -- resource not found.-----------------------------------------------------
84
108
 
85
109
  class NotFoundError < RuntimeError
86
110
  end
87
111
 
112
+ def not_found!(msg)
113
+ raise NotFoundError, msg
114
+ end
115
+
88
116
  error(NotFoundError) do |exc|
89
117
  render_error exc, status: 404,
90
118
  title: "Not found",
@@ -117,7 +145,7 @@ class Simple::Httpd::BaseController
117
145
  private
118
146
 
119
147
  def error_type(exc)
120
- "error/#{exc.class.name.underscore}".gsub(/\/error$/, "")
148
+ "error/#{H.underscore exc.class.name}".gsub(/\/error$/, "")
121
149
  end
122
150
 
123
151
  def error_description(exc)
@@ -29,18 +29,25 @@ class Simple::Httpd::BaseController
29
29
 
30
30
  public
31
31
 
32
- def parsed_json_body
33
- @parsed_json_body ||= parse_json_body
32
+ def parsed_body
33
+ return @parsed_body if defined? @parsed_body
34
+
35
+ @parsed_body = parse_body
36
+ rescue RuntimeError => e
37
+ raise ArgumentError, e.to_s
34
38
  end
35
39
 
36
40
  private
37
41
 
38
- def parse_json_body
39
- unless request.content_type =~ /application\/json/
40
- raise "Cannot parse non-JSON request body w/content_type #{request.content_type.inspect}"
42
+ def parse_body
43
+ case request.media_type
44
+ when "application/json"
45
+ request.body.rewind
46
+ body = request.body.read
47
+ body == "" ? {} : JSON.parse(body)
48
+ else
49
+ # parses form data
50
+ request.POST
41
51
  end
42
-
43
- request.body.rewind
44
- JSON.parse(request.body.read)
45
52
  end
46
53
  end
@@ -0,0 +1,99 @@
1
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
2
+
3
+ module Simple
4
+ class Httpd
5
+ class << self
6
+ attr_accessor :env
7
+ end
8
+ end
9
+ end
10
+
11
+ module Simple::Httpd::CLI
12
+ include Simple::CLI
13
+
14
+ # Runs a simple httpd server
15
+ #
16
+ # A mount_spec is either the location of a directory, which would then be mounted
17
+ # at the "/" HTTP location, or a directory followed by a colon and where to mount
18
+ # the directory.
19
+ #
20
+ # Mounted directories might contain either ruby source code which is then executed
21
+ # or static files to be delivered verbatim. See README.md for more details.
22
+ #
23
+ # Examples:
24
+ #
25
+ # simple-httpd --port=8080 httpd/root --service=src/to/service.rb MyService:/ httpd/assets:assets
26
+ #
27
+ # serves the content of ./httpd/root on http://0.0.0.0/ and the content of httpd/assets
28
+ # on http://0.0.0.0/assets.
29
+ #
30
+ # Options:
31
+ #
32
+ # --environment=ENV ... the environment setting, which adjusts configuration.
33
+ # --services=<path>,<path> ... load these ruby file during startup. Used to define service objects.
34
+ #
35
+ # simple-httpd respects the HOST and PORT environment values to determine the interface
36
+ # and port to listen to. Default values are "127.0.0.1" and 8181.
37
+ #
38
+ # Each entry in mounts can be either:
39
+ #
40
+ # - a mount_point <tt>[ mount_point, path ]</tt>, e.g. <tt>[ "path/to/root", "/"]</tt>
41
+ # - a string denoting a mount_point, e.g. "path/to/root:/")
42
+ # - a string denoting a "/" mount_point (e.g. "path", which is shorthand for "path:/")
43
+ def main(*mount_specs, environment: "development", services: nil)
44
+ ::Simple::Httpd.env = environment
45
+
46
+ start_simplecov if environment == "test"
47
+
48
+ mount_specs << "." if mount_specs.empty?
49
+
50
+ host = ENV["HOST"] || "127.0.0.1"
51
+ port = Integer(ENV["PORT"] || 8181)
52
+
53
+ # late loading simple/httpd, for simplecov support
54
+ require "simple/httpd"
55
+ helpers = ::Simple::Httpd::Helpers
56
+
57
+ services&.split(",")&.each do |service_path|
58
+ paths = if Dir.exist?(service_path)
59
+ Dir.glob("#{service_path}/**/*.rb").sort
60
+ else
61
+ [service_path]
62
+ end
63
+
64
+ paths.each do |path|
65
+ logger.info "Loading service(s) from #{helpers.shorten_path path}"
66
+ load path
67
+ end
68
+ end
69
+
70
+ ::Simple::Httpd.listen!(*mount_specs, environment: environment,
71
+ host: host,
72
+ port: port,
73
+ logger: logger)
74
+ end
75
+
76
+ private
77
+
78
+ def stderr_logger
79
+ logger = ::Logger.new STDERR
80
+ logger.level = ::Logger::INFO
81
+ logger
82
+ end
83
+
84
+ def start_simplecov
85
+ require "simplecov"
86
+
87
+ SimpleCov.command_name "Integration Tests"
88
+ SimpleCov.start do
89
+ # return true to remove src from coverage
90
+ add_filter do |src|
91
+ next true if src.filename =~ /\/spec\//
92
+
93
+ false
94
+ end
95
+
96
+ # minimum_coverage 90
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,54 @@
1
+ module Simple::Httpd::Helpers
2
+ extend self
3
+
4
+ private
5
+
6
+ def pwd
7
+ @pwd ||= File.join(Dir.getwd, "/")
8
+ end
9
+
10
+ def home
11
+ @home ||= File.join(Dir.home, "/")
12
+ end
13
+
14
+ public
15
+
16
+ def shorten_path(path)
17
+ path = File.absolute_path(path)
18
+
19
+ if path.start_with?(pwd)
20
+ path = path[pwd.length..-1]
21
+ path = File.join("./", path) if path =~ /\//
22
+ end
23
+
24
+ if path.start_with?(home)
25
+ path = File.join("~/", path[home.length..-1])
26
+ end
27
+
28
+ path
29
+ end
30
+
31
+ def underscore(str)
32
+ parts = str.split("::")
33
+ parts = parts.map do |part|
34
+ part.gsub(/[A-Z]+/) { |ch| "_#{ch.downcase}" }.gsub(/^_/, "")
35
+ end
36
+ parts.join("/")
37
+ end
38
+
39
+ # instance_eval zero or more paths in the context of obj
40
+ def instance_eval_paths(obj, *paths)
41
+ paths.each do |path|
42
+ # STDERR.puts "Loading #{path}"
43
+ obj.instance_eval File.read(path), path, 1
44
+ end
45
+ obj
46
+ end
47
+
48
+ # subclass a klass with an optional description
49
+ def subclass(klass, description: nil)
50
+ subclass = Class.new(klass)
51
+ subclass.define_method(:inspect) { description } if description
52
+ subclass
53
+ end
54
+ end
@@ -0,0 +1,106 @@
1
+ # rubocop:disable Metrics/AbcSize, Style/ParallelAssignment
2
+
3
+ require "simple-service"
4
+ require_relative "./rack"
5
+
6
+ class Simple::Httpd::MountSpec
7
+ def self.build(arg, at:)
8
+ if at
9
+ entity, mount_point = arg, at
10
+ else
11
+ # The regexp below uses negative lookbehind and negative lookahead to
12
+ # only match single colons, but not double (or more) colons. See
13
+ # `ri Regexp` for details.
14
+ entity, mount_point = *arg.split(/(?<!:):(?!:)/, 2)
15
+ end
16
+
17
+ mount_point = normalize_and_verify_mount_point(mount_point)
18
+
19
+ ServiceMountSpec.build(mount_point, service: entity) ||
20
+ PathMountSpec.build(mount_point, path: entity) ||
21
+ raise(ArgumentError, "#{mount_point}: don't know how to mount #{entity.inspect}")
22
+ end
23
+
24
+ def self.normalize_and_verify_mount_point(mount_point)
25
+ mount_point ||= "/" # fall back to "/"
26
+ mount_point = File.join("/", mount_point) # make sure we start at "/"
27
+
28
+ canary_url = "http://0.0.0.0#{mount_point}" # verify mount_point: can it be used to build a URL?
29
+ URI.parse canary_url
30
+
31
+ mount_point
32
+ end
33
+
34
+ attr_reader :mount_point
35
+
36
+ class PathMountSpec < ::Simple::Httpd::MountSpec
37
+ Rack = ::Simple::Httpd::Rack
38
+
39
+ attr_reader :path, :mount_point
40
+
41
+ def self.build(mount_point, path:)
42
+ path = path.gsub(/\/$/, "") # remove trailing "/"
43
+
44
+ raise ArgumentError, "You probably don't want to mount your root directory, check mount_spec" if path == ""
45
+ return unless Dir.exist?(path)
46
+
47
+ new(mount_point, path)
48
+ end
49
+
50
+ def initialize(mount_point, path)
51
+ @mount_point, @path = mount_point, path
52
+ end
53
+
54
+ def build_rack_apps
55
+ [
56
+ Rack::DynamicMount.build(mount_point, path),
57
+ Rack::StaticMount.build(mount_point, path)
58
+ ].compact
59
+ end
60
+ end
61
+
62
+ class ServiceMountSpec < ::Simple::Httpd::MountSpec
63
+ H = ::Simple::Httpd::Helpers
64
+
65
+ attr_reader :service
66
+
67
+ def self.build(mount_point, service:)
68
+ service = ::Simple::Service.resolve(service)
69
+ return unless service
70
+
71
+ new(mount_point, service)
72
+ end
73
+
74
+ def initialize(mount_point, service)
75
+ @mount_point, @service = mount_point, service
76
+ end
77
+
78
+ def build_rack_apps
79
+ [build_controller]
80
+ end
81
+
82
+ private
83
+
84
+ # wraps all helpers into a Simple::Httpd::BaseController subclass
85
+ def build_controller
86
+ controller = H.subclass(::Simple::Httpd::BaseController)
87
+ setup_action_routes! controller
88
+ controller
89
+ end
90
+
91
+ def setup_action_routes!(controller)
92
+ action_names = service.actions.keys
93
+
94
+ controller.mount_service(service) do |service|
95
+ action_names.each do |action_name|
96
+ ::Simple::Httpd.logger.debug "#{mount_point}/#{action_name} -> #{service.name}##{action_name}"
97
+ controller.post "/#{action_name}" => action_name
98
+ end
99
+ end
100
+
101
+ ::Simple::Httpd.logger.info do
102
+ "#{mount_point}: mounting #{action_names.count} actions(s) from #{service.name} service"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,17 @@
1
+ module Simple::Httpd::Rack
2
+ end
3
+
4
+ require_relative "rack/static_mount"
5
+ require_relative "rack/dynamic_mount"
6
+ require_relative "rack/merger"
7
+
8
+ module Simple::Httpd::Rack
9
+ def self.merge(apps)
10
+ Merger.build(apps)
11
+ end
12
+
13
+ def self.error(status, message = nil)
14
+ message ||= "Error #{status}"
15
+ [status, {}, [message]]
16
+ end
17
+ end
@@ -0,0 +1,66 @@
1
+ require "expectation"
2
+
3
+ # The Simple::Httpd::Mountpoint.build returns a Rack compatible app, which
4
+ # serves HTTP requests according to a set of dynamic ruby scripts and some
5
+ # existing static files.
6
+ class Simple::Httpd::Rack::DynamicMount
7
+ H = ::Simple::Httpd::Helpers
8
+ Rack = ::Simple::Httpd::Rack
9
+
10
+ def self.build(mount_point, path)
11
+ expect! path => String
12
+
13
+ url_map = new(mount_point, path).build_url_map
14
+
15
+ ::Rack::URLMap.new(url_map)
16
+ end
17
+
18
+ attr_reader :path
19
+ attr_reader :mount_point
20
+
21
+ def initialize(mount_point, path)
22
+ @mount_point = mount_point
23
+ @path = path
24
+
25
+ # determine source_paths and controller_paths
26
+ source_paths = Dir.glob("#{path}/**/*.rb")
27
+ helper_paths, @controller_paths = source_paths.partition { |str| /_helpers\.rb$/ =~ str }
28
+
29
+ # build root_controller
30
+ @root_controller = build_root_controller(helper_paths)
31
+ end
32
+
33
+ # rubocop:disable Metrics/AbcSize
34
+ def build_url_map
35
+ @controller_paths.sort.each_with_object({}) do |absolute_path, url_map|
36
+ relative_path = absolute_path[(path.length)..-1]
37
+
38
+ relative_mount_point = relative_path == "/root.rb" ? "/" : relative_path.gsub(/\.rb$/, "")
39
+ controller_class = load_controller absolute_path
40
+
41
+ ::Simple::Httpd.logger.info do
42
+ absolute_mount_point = File.join(mount_point, relative_mount_point)
43
+ routes_count = controller_class.routes.reject { |verb, _| verb == "HEAD" }.values.sum(&:count)
44
+
45
+ "#{absolute_mount_point}: mounting #{routes_count} route(s) from #{H.shorten_path absolute_path}"
46
+ end
47
+
48
+ url_map.update relative_mount_point => controller_class
49
+ end
50
+ end
51
+
52
+ # wraps all helpers into a Simple::Httpd::BaseController subclass
53
+ def build_root_controller(helper_paths)
54
+ klass = H.subclass ::Simple::Httpd::BaseController,
55
+ description: "root controller at #{path} w/#{helper_paths.count} helpers"
56
+
57
+ H.instance_eval_paths klass, *helper_paths.sort
58
+ klass
59
+ end
60
+
61
+ # wraps the source file in path into a root_controller
62
+ def load_controller(path)
63
+ klass = H.subclass @root_controller, description: "controller at #{path}"
64
+ H.instance_eval_paths klass, path
65
+ end
66
+ end