simple-httpd 0.3.0 → 0.3.1

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