simple-httpd 0.3.0 → 0.3.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -0
  3. data/Gemfile +3 -1
  4. data/VERSION +1 -1
  5. data/bin/simple-httpd +1 -3
  6. data/examples/ex1.services/ex1_service_module.rb +8 -0
  7. data/examples/ex1/root.rb +4 -0
  8. data/examples/ex1/spec.rb +7 -0
  9. data/examples/ex2/info.rb +5 -3
  10. data/examples/ex3/example_service.rb +3 -2
  11. data/lib/simple/httpd.rb +37 -28
  12. data/lib/simple/httpd/base_controller.rb +6 -0
  13. data/lib/simple/httpd/base_controller/build_url.rb +1 -1
  14. data/lib/simple/httpd/base_controller/debug.rb +2 -0
  15. data/lib/simple/httpd/base_controller/error_handling.rb +21 -38
  16. data/lib/simple/httpd/base_controller/json.rb +2 -31
  17. data/lib/simple/httpd/base_controller/request.rb +23 -0
  18. data/lib/simple/httpd/base_controller/result.rb +19 -0
  19. data/lib/simple/httpd/cli.rb +71 -30
  20. data/lib/simple/httpd/helpers.rb +35 -3
  21. data/lib/simple/httpd/mount.rb +66 -0
  22. data/lib/simple/httpd/rack/dynamic_mount.rb +52 -32
  23. data/lib/simple/httpd/rack/static_mount.rb +20 -14
  24. data/lib/simple/httpd/route.rb +79 -0
  25. data/lib/simple/httpd/server.rb +28 -16
  26. data/lib/simple/httpd/service_adapter.rb +108 -0
  27. data/simple-httpd.gemspec +2 -1
  28. data/spec/simple/httpd/base_controller/httpd_cors_spec.rb +8 -3
  29. data/spec/simple/httpd/base_controller/httpd_url_building_spec.rb +17 -0
  30. data/spec/simple/httpd/helpers_spec.rb +25 -8
  31. data/spec/simple/httpd/loading_helpers_spec.rb +15 -0
  32. data/spec/simple/httpd/rspec_httpd_spec.rb +25 -9
  33. data/spec/simple/httpd/services/service_explicit_spec.rb +0 -5
  34. data/spec/simple/httpd/services/sideloading_spec.rb +9 -0
  35. data/spec/spec_helper.rb +2 -3
  36. metadata +31 -12
  37. data/examples/services/example_service.rb +0 -25
  38. data/lib/simple/httpd/mount_spec.rb +0 -106
  39. data/lib/simple/httpd/service.rb +0 -70
  40. data/lib/simple/service.rb +0 -69
  41. data/lib/simple/service/action.rb +0 -78
  42. data/lib/simple/service/context.rb +0 -46
  43. data/spec/simple/httpd/services/service_spec.rb +0 -34
@@ -1,5 +1,3 @@
1
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
2
-
3
1
  module Simple
4
2
  class Httpd
5
3
  class << self
@@ -11,9 +9,13 @@ end
11
9
  module Simple::Httpd::CLI
12
10
  include Simple::CLI
13
11
 
12
+ def logger
13
+ ::Simple::CLI.logger
14
+ end
15
+
14
16
  # Runs a simple httpd server
15
17
  #
16
- # A mount_spec is either the location of a directory, which would then be mounted
18
+ # A mount is either the location of a directory, which would then be mounted
17
19
  # at the "/" HTTP location, or a directory followed by a colon and where to mount
18
20
  # the directory.
19
21
  #
@@ -22,7 +24,9 @@ module Simple::Httpd::CLI
22
24
  #
23
25
  # Examples:
24
26
  #
25
- # simple-httpd --port=8080 httpd/root --service=src/to/service.rb MyService:/ httpd/assets:assets
27
+ # PORT=8080 simple-httpd start httpd/root --service=src/to/service.rb \
28
+ # MyService:/
29
+ # httpd/assets:assets
26
30
  #
27
31
  # serves the content of ./httpd/root on http://0.0.0.0/ and the content of httpd/assets
28
32
  # on http://0.0.0.0/assets.
@@ -30,7 +34,8 @@ module Simple::Httpd::CLI
30
34
  # Options:
31
35
  #
32
36
  # --environment=ENV ... the environment setting, which adjusts configuration.
33
- # --services=<path>,<path> ... load these ruby file during startup. Used to define service objects.
37
+ # --services=<path>,<path> ... load these ruby files or directories during startup. This
38
+ # can be used to define service objects.
34
39
  #
35
40
  # simple-httpd respects the HOST and PORT environment values to determine the interface
36
41
  # and port to listen to. Default values are "127.0.0.1" and 8181.
@@ -40,40 +45,76 @@ module Simple::Httpd::CLI
40
45
  # - a mount_point <tt>[ mount_point, path ]</tt>, e.g. <tt>[ "path/to/root", "/"]</tt>
41
46
  # - a string denoting a mount_point, e.g. "path/to/root:/")
42
47
  # - 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
48
+ def start(*mounts, environment: "development", services: nil)
49
+ host = ENV["HOST"] || "127.0.0.1"
50
+ port = Integer(ENV["PORT"] || 8181)
45
51
 
46
- start_simplecov if environment == "test"
52
+ prepare_environment!(environment: environment)
53
+
54
+ app = build_app!(mounts: mounts, services: services)
55
+ logger.info "start to listen on #{mounts.inspect}"
56
+ ::Simple::Httpd.listen!(app, environment: environment,
57
+ host: host,
58
+ port: port)
59
+ end
47
60
 
48
- mount_specs << "." if mount_specs.empty?
61
+ def routes(*mounts, environment: "development", services: nil)
62
+ prepare_environment!(environment: environment)
63
+ app = build_app!(mounts: mounts, services: services)
64
+ routes = app.route_descriptions
49
65
 
50
- host = ENV["HOST"] || "127.0.0.1"
51
- port = Integer(ENV["PORT"] || 8181)
66
+ logger.info "Found #{routes.count} routes"
67
+
68
+ max_verb_len = routes.map(&:verb).map(&:length).max
69
+ max_path_len = routes.map(&:path).map(&:length).max
70
+
71
+ routes.
72
+ sort_by { |route| [route.path, route.verb] }.
73
+ each { |route|
74
+ puts format("%#{max_verb_len}s %-#{max_path_len}s %s", route.verb, route.path, route.source_location_str)
75
+ }
76
+ end
77
+
78
+ private
79
+
80
+ def prepare_environment!(environment:)
81
+ ::Simple::Httpd.env = environment
82
+ start_simplecov if environment == "test"
52
83
 
53
84
  # late loading simple/httpd, for simplecov support
54
85
  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
86
+ end
69
87
 
70
- ::Simple::Httpd.listen!(*mount_specs, environment: environment,
71
- host: host,
72
- port: port,
73
- logger: logger)
88
+ def build_app!(mounts:, services:)
89
+ mounts << "." if mounts.empty?
90
+ logger.info "building server on #{mounts.inspect}"
91
+
92
+ load_services! services if services
93
+ app = ::Simple::Httpd.build(*mounts)
94
+ app.rack # builds the rack application
95
+ app
74
96
  end
75
97
 
76
- private
98
+ def load_services!(paths)
99
+ expect! paths => String
100
+
101
+ resolve_service_path(paths).each do |path|
102
+ logger.info "Loading service(s) from #{::Simple::Httpd::Helpers.shorten_path path}"
103
+ load path
104
+ end
105
+ end
106
+
107
+ def resolve_service_path(paths)
108
+ # each service_path either denotes the path to a file or to a directory
109
+ # of files.
110
+ paths.split(",").each_with_object([]) do |service_path, ary|
111
+ if Dir.exist?(service_path)
112
+ ary.concat Dir.glob("#{service_path}/**/*.rb").sort
113
+ else
114
+ ary << service_path
115
+ end
116
+ end
117
+ end
77
118
 
78
119
  def stderr_logger
79
120
  logger = ::Logger.new STDERR
@@ -37,8 +37,10 @@ module Simple::Httpd::Helpers
37
37
  end
38
38
 
39
39
  # instance_eval zero or more paths in the context of obj
40
- def instance_eval_paths(obj, *paths)
41
- paths.each do |path|
40
+ def instance_eval_paths(obj, paths:)
41
+ return obj unless paths
42
+
43
+ Array(paths).each do |path|
42
44
  # STDERR.puts "Loading #{path}"
43
45
  obj.instance_eval File.read(path), path, 1
44
46
  end
@@ -46,9 +48,39 @@ module Simple::Httpd::Helpers
46
48
  end
47
49
 
48
50
  # subclass a klass with an optional description
49
- def subclass(klass, description: nil)
51
+ def subclass(klass, paths: nil, description: nil)
52
+ raise "Missing description" unless description
53
+
50
54
  subclass = Class.new(klass)
51
55
  subclass.define_method(:inspect) { description } if description
56
+ instance_eval_paths subclass, paths: paths if paths
52
57
  subclass
53
58
  end
59
+
60
+ def filter_stacktrace_entry?(line)
61
+ return true if line =~ /\.rvm\b/
62
+
63
+ false
64
+ end
65
+
66
+ # Receives a stacktrace (like, for example, from Kernel#callers or
67
+ # from Exception#backtrace), and removes all lines that point to
68
+ # ".rvm". It also removes the working directory from the file paths.
69
+ #
70
+ # returns the cleaned array
71
+ def filtered_stacktrace(stacktrace, count: 20)
72
+ lines = []
73
+
74
+ stacktrace[0..count].inject(false) do |filtered_last_line, line|
75
+ if filter_stacktrace_entry?(line)
76
+ lines << "... (lines removed) ..." unless filtered_last_line
77
+ true
78
+ else
79
+ lines << shorten_path(line)
80
+ false
81
+ end
82
+ end
83
+
84
+ lines
85
+ end
54
86
  end
@@ -0,0 +1,66 @@
1
+ require "simple-service"
2
+ require_relative "./rack"
3
+
4
+ module Simple::Httpd::Mount
5
+ extend self
6
+
7
+ def 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
+ PathMount.build(mount_point, path: entity) ||
20
+ raise(ArgumentError, "#{mount_point}: don't know how to mount #{entity.inspect}")
21
+ end
22
+
23
+ private
24
+
25
+ def normalize_and_verify_mount_point(mount_point)
26
+ mount_point ||= "/" # fall back to "/"
27
+ mount_point = File.join("/", mount_point) # make sure we start at "/"
28
+
29
+ canary_url = "http://0.0.0.0#{mount_point}" # verify mount_point: can it be used to build a URL?
30
+ URI.parse canary_url
31
+
32
+ mount_point
33
+ end
34
+
35
+ class PathMount
36
+ Rack = ::Simple::Httpd::Rack
37
+
38
+ attr_reader :path, :mount_point
39
+
40
+ def self.build(mount_point, path:)
41
+ path = path.gsub(/\/$/, "") # remove trailing "/"
42
+
43
+ raise ArgumentError, "You probably don't want to mount your root directory, check mount" if path == ""
44
+ return unless Dir.exist?(path)
45
+
46
+ new(mount_point, path)
47
+ end
48
+
49
+ def initialize(mount_point, path)
50
+ @mount_point, @path = mount_point, path
51
+ end
52
+
53
+ def route_descriptions
54
+ build_rack_apps.inject([]) do |ary, app|
55
+ ary.concat app.route_descriptions
56
+ end
57
+ end
58
+
59
+ def build_rack_apps
60
+ dynamic_mount = Rack::DynamicMount.build(mount_point, path)
61
+ static_mount = Rack::StaticMount.build(mount_point, path)
62
+
63
+ [dynamic_mount, static_mount].compact
64
+ end
65
+ end
66
+ end
@@ -7,12 +7,13 @@ class Simple::Httpd::Rack::DynamicMount
7
7
  H = ::Simple::Httpd::Helpers
8
8
  Rack = ::Simple::Httpd::Rack
9
9
 
10
- def self.build(mount_point, path)
11
- expect! path => String
10
+ extend Forwardable
12
11
 
13
- url_map = new(mount_point, path).build_url_map
12
+ delegate :call => :@rack_app # rubocop:disable Style/HashSyntax
14
13
 
15
- ::Rack::URLMap.new(url_map)
14
+ def self.build(mount_point, path)
15
+ expect! path => String
16
+ new(mount_point, path)
16
17
  end
17
18
 
18
19
  attr_reader :path
@@ -20,47 +21,66 @@ class Simple::Httpd::Rack::DynamicMount
20
21
 
21
22
  def initialize(mount_point, path)
22
23
  @mount_point = mount_point
23
- @path = path
24
+ @path = path.gsub(/\/\z/, "") # remove trailing "/"
24
25
 
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 }
26
+ setup_paths!
27
+ load_service_files!
28
+ @root_controller = build_root_controller # also loads helpers
29
+ @url_map = build_url_map
28
30
 
29
- # build root_controller
30
- @root_controller = build_root_controller(helper_paths)
31
+ @rack_app = ::Rack::URLMap.new(@url_map)
31
32
  end
32
33
 
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]
34
+ # RouteDescriptions are being built during build_url_map
35
+ include ::Simple::Httpd::RouteDescriptions
37
36
 
38
- relative_mount_point = relative_path == "/root.rb" ? "/" : relative_path.gsub(/\.rb$/, "")
39
- controller_class = load_controller absolute_path
37
+ private
40
38
 
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)
39
+ def logger
40
+ ::Simple::Httpd.logger
41
+ end
44
42
 
45
- "#{absolute_mount_point}: mounting #{routes_count} route(s) from #{H.shorten_path absolute_path}"
46
- end
43
+ def setup_paths!
44
+ @source_paths = Dir.glob("#{path}/**/*.rb")
45
+ @helper_paths, @controller_paths = @source_paths.partition { |str| /_helper(s?)\.rb$/ =~ str }
46
+
47
+ logger.info "#{path}: found #{@source_paths.count} sources, #{@helper_paths.count} helpers"
48
+ end
47
49
 
48
- url_map.update relative_mount_point => controller_class
50
+ def load_service_files!
51
+ return if path == "." # i.e. mounting current directory
52
+
53
+ service_path = "#{path}.services"
54
+ service_files = Dir.glob("#{service_path}/**/*.rb")
55
+ return if service_files.empty?
56
+
57
+ logger.info "#{service_path}: loading #{service_files.count} service file(s)"
58
+ service_files.sort.each do |path|
59
+ logger.debug "Loading service file #{path.inspect}"
60
+ load path
49
61
  end
50
62
  end
51
63
 
52
64
  # 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
65
+ def build_root_controller
66
+ H.subclass ::Simple::Httpd::BaseController,
67
+ paths: @helper_paths.sort,
68
+ description: "root controller at #{path} w/#{@helper_paths.count} helpers"
59
69
  end
60
70
 
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
71
+ def build_url_map
72
+ @controller_paths.sort.each_with_object({}) do |absolute_path, hsh|
73
+ relative_path = absolute_path[(path.length)..-1]
74
+
75
+ relative_mount_point = relative_path == "/root.rb" ? "/" : relative_path.gsub(/\.rb$/, "")
76
+ controller_class = H.subclass @root_controller, description: "controller at #{absolute_path}", paths: absolute_path
77
+
78
+ controller_class.route_descriptions.each do |route|
79
+ route = route.prefix(@mount_point, relative_mount_point)
80
+ describe_route! route
81
+ end
82
+
83
+ hsh.update relative_mount_point => controller_class
84
+ end
65
85
  end
66
86
  end
@@ -1,48 +1,54 @@
1
1
  # A simple file server middleware
2
2
  class Simple::Httpd::Rack::StaticMount
3
+ H = ::Simple::Httpd::Helpers
3
4
  Rack = ::Simple::Httpd::Rack
4
5
 
5
6
  EXTENSIONS = %w(.txt .md .js .css .png .jpeg .jpg)
7
+ GLOB_PATTERN = "**/*.{#{EXTENSIONS.map { |s| s[1..-1] }.join(",")}}"
6
8
 
7
9
  def self.build(mount_point, path)
8
- static_files = static_files(path)
10
+ static_files = Dir.chdir(path) { Dir.glob(GLOB_PATTERN) }
11
+
9
12
  return nil if static_files.empty?
10
13
 
11
14
  ::Simple::Httpd.logger.info do
12
15
  "#{mount_point}: serving #{static_files.count} static file(s)"
13
16
  end
14
17
 
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
18
+ new(mount_point, path, static_files)
23
19
  end
24
20
 
25
21
  attr_reader :mount_point, :path
26
22
 
27
- def initialize(path, static_files)
23
+ private
24
+
25
+ def initialize(mount_point, path, static_files)
26
+ @mount_point = mount_point
28
27
  @path = path
29
28
  @static_files = Set.new(static_files)
29
+ @file_server = ::Rack::File.new(path)
30
+
31
+ describe_route! verb: "GET",
32
+ path: File.join(mount_point, GLOB_PATTERN),
33
+ source_location: File.join(H.shorten_path(path), GLOB_PATTERN)
30
34
  end
31
35
 
36
+ include ::Simple::Httpd::RouteDescriptions
37
+
38
+ public
39
+
32
40
  def call(env)
33
41
  request_path = env["PATH_INFO"]
34
42
  if serve_file?(request_path)
35
43
  file_path = request_path[1..-1]
36
44
  env["PATH_INFO"] = file_path
37
- file_server.call(env)
45
+ @file_server.call(env)
38
46
  else
39
47
  Rack.error 404, "No such file"
40
48
  end
41
49
  end
42
50
 
43
- def file_server
44
- @file_server ||= ::Rack::File.new(path)
45
- end
51
+ private
46
52
 
47
53
  def serve_file?(request_path)
48
54
  @static_files.include?(request_path[1..-1])
@@ -0,0 +1,79 @@
1
+ class Simple::Httpd::Route
2
+ H = ::Simple::Httpd::Helpers
3
+ SELF = self
4
+
5
+ attr_reader :verb, :path, :source_location
6
+
7
+ def self.build(route)
8
+ expect! route => [
9
+ ::Simple::Httpd::Route,
10
+ {
11
+ verb: %w(GET POST PUT DELETE HEAD OPTIONS),
12
+ path: String,
13
+ source_location: [nil, Array, String]
14
+ }
15
+ ]
16
+
17
+ return route if route.is_a?(self)
18
+
19
+ ::Simple::Httpd::Route.new(*route.values_at(:verb, :path, :source_location))
20
+ end
21
+
22
+ private
23
+
24
+ def initialize(verb, path, source_location)
25
+ @verb = verb
26
+ @path = path
27
+ @source_location = source_location
28
+ end
29
+
30
+ public
31
+
32
+ def to_s
33
+ parts = [verb, path, source_location_str]
34
+ parts.compact.join " "
35
+ end
36
+
37
+ def inspect
38
+ "<#{SELF.name}: #{self}>"
39
+ end
40
+
41
+ def prefix(*prefixes)
42
+ SELF.new(verb, File.join(*prefixes, path), source_location)
43
+ end
44
+
45
+ def source_location_str
46
+ case source_location
47
+ when Array
48
+ path, lineno = *source_location
49
+ path = H.shorten_path(path)
50
+ "#{path}:#{lineno}"
51
+ when String, nil
52
+ source_location
53
+ else
54
+ source_location.inspect
55
+ end
56
+ end
57
+ end
58
+
59
+ module Simple::Httpd::RouteDescriptions
60
+ # returns a list of route desc entries.
61
+ #
62
+ # When building a simple-httpd application we also collect all routes defined
63
+ # via get, put, etc., including their source location (which, for example,
64
+ # might point to a service's method or to a controller source file).
65
+ #
66
+ # (see <tt>Simple::Httpd::Route</tt>).
67
+ def route_descriptions
68
+ @route_descriptions ||= []
69
+ end
70
+
71
+ # Adds a route description.
72
+ #
73
+ # The argument must either be a Route, or an argument acceptable to Route.build.
74
+ #
75
+ # (see <tt>Simple::Httpd::Route</tt>).
76
+ def describe_route!(route)
77
+ route_descriptions << ::Simple::Httpd::Route.build(route)
78
+ end
79
+ end