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.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/Gemfile +3 -1
- data/VERSION +1 -1
- data/bin/simple-httpd +1 -3
- data/examples/ex1.services/ex1_service_module.rb +8 -0
- data/examples/ex1/root.rb +4 -0
- data/examples/ex1/spec.rb +7 -0
- data/examples/ex2/info.rb +5 -3
- data/examples/ex3/example_service.rb +3 -2
- data/lib/simple/httpd.rb +37 -28
- data/lib/simple/httpd/base_controller.rb +6 -0
- data/lib/simple/httpd/base_controller/build_url.rb +1 -1
- data/lib/simple/httpd/base_controller/debug.rb +2 -0
- data/lib/simple/httpd/base_controller/error_handling.rb +21 -38
- data/lib/simple/httpd/base_controller/json.rb +2 -31
- data/lib/simple/httpd/base_controller/request.rb +23 -0
- data/lib/simple/httpd/base_controller/result.rb +19 -0
- data/lib/simple/httpd/cli.rb +71 -30
- data/lib/simple/httpd/helpers.rb +35 -3
- data/lib/simple/httpd/mount.rb +66 -0
- data/lib/simple/httpd/rack/dynamic_mount.rb +52 -32
- data/lib/simple/httpd/rack/static_mount.rb +20 -14
- data/lib/simple/httpd/route.rb +79 -0
- data/lib/simple/httpd/server.rb +28 -16
- data/lib/simple/httpd/service_adapter.rb +108 -0
- data/simple-httpd.gemspec +2 -1
- data/spec/simple/httpd/base_controller/httpd_cors_spec.rb +8 -3
- data/spec/simple/httpd/base_controller/httpd_url_building_spec.rb +17 -0
- data/spec/simple/httpd/helpers_spec.rb +25 -8
- data/spec/simple/httpd/loading_helpers_spec.rb +15 -0
- data/spec/simple/httpd/rspec_httpd_spec.rb +25 -9
- data/spec/simple/httpd/services/service_explicit_spec.rb +0 -5
- data/spec/simple/httpd/services/sideloading_spec.rb +9 -0
- data/spec/spec_helper.rb +2 -3
- metadata +31 -12
- data/examples/services/example_service.rb +0 -25
- data/lib/simple/httpd/mount_spec.rb +0 -106
- data/lib/simple/httpd/service.rb +0 -70
- data/lib/simple/service.rb +0 -69
- data/lib/simple/service/action.rb +0 -78
- data/lib/simple/service/context.rb +0 -46
- 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
|
data/lib/simple/httpd/service.rb
DELETED
@@ -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
|
data/lib/simple/service.rb
DELETED
@@ -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
|