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,25 +0,0 @@
1
- # rubocop:disable Naming/UncommunicativeMethodParamName, Lint/UnusedMethodArgument
2
-
3
- module Example; end
4
- module Example::Service
5
- include ::Simple::Service
6
-
7
- def test(a:, b:)
8
- # "this is a test; a is #{a.inspect}, b is #{b.inspect}"
9
- "hello from ExampleService#test"
10
- end
11
-
12
- def echo(one, two, a:, b:)
13
- "one: [#{one}]/two: [#{two}]/a: [#{a}]/b: [#{b}]"
14
- end
15
-
16
- def echo_context
17
- ::Simple::Service.context.inspect
18
- end
19
-
20
- def this_is_a_helper!; end
21
-
22
- private
23
-
24
- def this_is_a_private_helper; end
25
- end
@@ -1,106 +0,0 @@
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
@@ -1,70 +0,0 @@
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,69 +0,0 @@
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
@@ -1,78 +0,0 @@
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
@@ -1,46 +0,0 @@
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
@@ -1,34 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe "service file" do
4
- # mounting not at root level
5
- it "returns value from mapped function" do
6
- http.post "/service/example/test?a=1&b=2"
7
- expect_response("hello from ExampleService#test")
8
- end
9
-
10
- it "properly extracts arguments and parameters" do
11
- http.post "/service/example/echo?a=1&b=2", { one: "foo", two: "bar" }
12
- expect_response "one: [foo]/two: [bar]/a: [1]/b: [2]"
13
- end
14
-
15
- it "ignores extra body arguments and extra parameters" do
16
- http.post "/service/example/echo?a=1&b=2&c=3", { one: "foo", two: "bar", three: "baz" }
17
- expect_response "one: [foo]/two: [bar]/a: [1]/b: [2]"
18
- end
19
-
20
- it "complains on missing body arguments" do
21
- http.post "/service/example/echo?a=1&b=2&c=3", { two: "bar" }
22
- expect_response 422
23
- end
24
-
25
- it "ignores missing parameters arguments" do
26
- http.post "/service/example/echo?b=2", { one: "foo", two: "bar" }
27
- expect_response "one: [foo]/two: [bar]/a: []/b: [2]"
28
- end
29
-
30
- it "properly extracts arguments and parameters" do
31
- http.post "/service/example/echo_context"
32
- expect_response /Simple::Service::Context/
33
- end
34
- end