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