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