simple-service 0.1.4
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 +7 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +100 -0
- data/.tm_properties +1 -0
- data/Gemfile +14 -0
- data/Makefile +9 -0
- data/README.md +68 -0
- data/Rakefile +6 -0
- data/VERSION +1 -0
- data/bin/bundle +105 -0
- data/bin/console +15 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/doc/Simple.html +117 -0
- data/doc/Simple/Service.html +863 -0
- data/doc/Simple/Service/Action.html +1014 -0
- data/doc/Simple/Service/Action/Comment.html +451 -0
- data/doc/Simple/Service/Action/Comment/Extractor.html +347 -0
- data/doc/Simple/Service/Action/IndieHash.html +506 -0
- data/doc/Simple/Service/Action/MethodReflection.html +285 -0
- data/doc/Simple/Service/Action/Parameter.html +816 -0
- data/doc/Simple/Service/ArgumentError.html +128 -0
- data/doc/Simple/Service/ClassMethods.html +187 -0
- data/doc/Simple/Service/Context.html +379 -0
- data/doc/Simple/Service/ContextMissingError.html +124 -0
- data/doc/Simple/Service/ContextReadOnlyError.html +206 -0
- data/doc/Simple/Service/ExtraArguments.html +428 -0
- data/doc/Simple/Service/GemHelper.html +190 -0
- data/doc/Simple/Service/MissingArguments.html +426 -0
- data/doc/Simple/Service/NoSuchAction.html +433 -0
- data/doc/_index.html +286 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +496 -0
- data/doc/file.README.html +146 -0
- data/doc/file_list.html +56 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +146 -0
- data/doc/js/app.js +303 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +539 -0
- data/doc/top-level-namespace.html +110 -0
- data/lib/simple-service.rb +3 -0
- data/lib/simple/service.rb +143 -0
- data/lib/simple/service/action.rb +203 -0
- data/lib/simple/service/action/comment.rb +57 -0
- data/lib/simple/service/action/indie_hash.rb +37 -0
- data/lib/simple/service/action/method_reflection.rb +70 -0
- data/lib/simple/service/action/parameter.rb +42 -0
- data/lib/simple/service/context.rb +94 -0
- data/lib/simple/service/errors.rb +54 -0
- data/lib/simple/service/version.rb +29 -0
- data/log/.gitkeep +0 -0
- data/scripts/release +2 -0
- data/scripts/release.rb +91 -0
- data/scripts/stats +5 -0
- data/scripts/watch +2 -0
- data/simple-service.gemspec +25 -0
- data/spec/simple/service/action_invoke2_spec.rb +166 -0
- data/spec/simple/service/action_invoke_spec.rb +266 -0
- data/spec/simple/service/action_spec.rb +51 -0
- data/spec/simple/service/context_spec.rb +69 -0
- data/spec/simple/service/service_spec.rb +105 -0
- data/spec/simple/service/version_spec.rb +7 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/spec_services.rb +50 -0
- metadata +130 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
# returns the comment for an action
|
2
|
+
class ::Simple::Service::Action::Comment # @private
|
3
|
+
attr_reader :short
|
4
|
+
attr_reader :full
|
5
|
+
|
6
|
+
def self.extract(action:)
|
7
|
+
file, line = action.source_location
|
8
|
+
lines = Extractor.extract_comment_lines(file: file, before_line: line)
|
9
|
+
full = lines[2..-1].join("\n") if lines.length >= 2
|
10
|
+
new short: lines[0], full: full
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(short:, full:)
|
14
|
+
@short, @full = short, full
|
15
|
+
end
|
16
|
+
|
17
|
+
module Extractor
|
18
|
+
extend self
|
19
|
+
|
20
|
+
# reads the source \a file and turns each non-comment into :code and each comment
|
21
|
+
# into a string without the leading comment markup.
|
22
|
+
def parse_source(file)
|
23
|
+
@parsed_sources ||= {}
|
24
|
+
@parsed_sources[file] = _parse_source(file)
|
25
|
+
end
|
26
|
+
|
27
|
+
def _parse_source(file)
|
28
|
+
File.readlines(file).map do |line|
|
29
|
+
case line
|
30
|
+
when /^\s*# ?(.*)$/ then $1
|
31
|
+
when /^\s*end/ then :end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_comment_lines(file:, before_line:)
|
37
|
+
parsed_source = parse_source(file)
|
38
|
+
|
39
|
+
# go down from before_line until we see a line which is either a comment
|
40
|
+
# or an :end. Note that the line at before_line-1 should be the first
|
41
|
+
# line of the method definition in question.
|
42
|
+
last_line = before_line - 1
|
43
|
+
last_line -= 1 while last_line >= 0 && !parsed_source[last_line]
|
44
|
+
|
45
|
+
first_line = last_line
|
46
|
+
first_line -= 1 while first_line >= 0 && parsed_source[first_line]
|
47
|
+
first_line += 1
|
48
|
+
|
49
|
+
comments = parsed_source[first_line..last_line]
|
50
|
+
if comments.include?(:end)
|
51
|
+
[]
|
52
|
+
else
|
53
|
+
parsed_source[first_line..last_line]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Simple::Service::Action
|
2
|
+
# The IndieHash class defines as much of the Hash interface as necessary for simple-service
|
3
|
+
# to successfully run.
|
4
|
+
class IndieHash
|
5
|
+
def initialize(hsh)
|
6
|
+
@hsh = hsh.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
7
|
+
end
|
8
|
+
|
9
|
+
def keys
|
10
|
+
@hsh.keys
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch_values(*keys)
|
14
|
+
keys = keys.map(&:to_s)
|
15
|
+
@hsh.fetch_values(*keys)
|
16
|
+
end
|
17
|
+
|
18
|
+
def key?(sym)
|
19
|
+
@hsh.key?(sym.to_s)
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](sym)
|
23
|
+
@hsh[sym.to_s]
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge(other_hsh)
|
27
|
+
@hsh = @hsh.merge(other_hsh.send(:__hsh__))
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def __hsh__
|
34
|
+
@hsh
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize
|
2
|
+
|
3
|
+
module ::Simple::Service::Action::MethodReflection # @private
|
4
|
+
extend self
|
5
|
+
|
6
|
+
#
|
7
|
+
# returns an array with entries like the following:
|
8
|
+
#
|
9
|
+
# [ :key, name, default_value ]
|
10
|
+
# [ :keyreq, name [, nil ] ]
|
11
|
+
# [ :req, name [, nil ] ]
|
12
|
+
# [ :opt, name [, nil ] ]
|
13
|
+
# [ :rest, name [, nil ] ]
|
14
|
+
#
|
15
|
+
def parameters(service, method_id)
|
16
|
+
method = service.instance_method(method_id)
|
17
|
+
parameters = method.parameters
|
18
|
+
|
19
|
+
# method parameters with a :key mode are optional named arguments. We only
|
20
|
+
# support defaults for those - if there are none we abort here already.
|
21
|
+
keys = parameters.map { |mode, name| name if mode == :key }.compact
|
22
|
+
return parameters if keys.empty?
|
23
|
+
|
24
|
+
# We are now doing a fake call to the method, with a minimal viable set of
|
25
|
+
# arguments, to let the ruby runtime fill in default values for arguments.
|
26
|
+
# We do not, however, let the call complete. Instead we use a TracePoint to
|
27
|
+
# abort as soon as the method is called, and use the its binding to determine
|
28
|
+
# the default values.
|
29
|
+
|
30
|
+
fake_recipient = Object.new.extend(service)
|
31
|
+
fake_call_args = minimal_arguments(method)
|
32
|
+
|
33
|
+
trace_point = TracePoint.trace(:call) do |tp|
|
34
|
+
throw :received_fake_call, tp.binding if tp.defined_class == service && tp.method_id == method_id
|
35
|
+
end
|
36
|
+
|
37
|
+
bnd = catch(:received_fake_call) do
|
38
|
+
fake_recipient.send(method_id, *fake_call_args)
|
39
|
+
end
|
40
|
+
|
41
|
+
trace_point.disable
|
42
|
+
|
43
|
+
# extract default values from the received binding, and merge with the
|
44
|
+
# parameters array.
|
45
|
+
default_values = keys.each_with_object({}) do |key_parameter, hsh|
|
46
|
+
hsh[key_parameter] = bnd.local_variable_get(key_parameter)
|
47
|
+
end
|
48
|
+
|
49
|
+
parameters.map do |mode, name|
|
50
|
+
[mode, name, default_values[name]]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# returns a minimal Array of arguments, which is suitable for a call to the method
|
57
|
+
def minimal_arguments(method)
|
58
|
+
# Build an arguments array with holds all required parameters. The actual
|
59
|
+
# values for these arguments doesn't matter at all.
|
60
|
+
args = method.parameters.select { |mode, _name| mode == :req }
|
61
|
+
|
62
|
+
# Add a hash with all required named arguments
|
63
|
+
required_keyword_args = method.parameters.each_with_object({}) do |(mode, name), hsh|
|
64
|
+
hsh[name] = :anything if mode == :keyreq
|
65
|
+
end
|
66
|
+
args << required_keyword_args if required_keyword_args
|
67
|
+
|
68
|
+
args
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative "method_reflection"
|
2
|
+
|
3
|
+
class ::Simple::Service::Action::Parameter
|
4
|
+
def self.reflect_on_method(service:, name:)
|
5
|
+
reflected_parameters = ::Simple::Service::Action::MethodReflection.parameters(service, name)
|
6
|
+
@parameters = reflected_parameters.map { |ary| new(*ary) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def keyword?
|
10
|
+
[:keyreq, :key].include? @kind
|
11
|
+
end
|
12
|
+
|
13
|
+
def positional?
|
14
|
+
[:req, :opt].include? @kind
|
15
|
+
end
|
16
|
+
|
17
|
+
def required?
|
18
|
+
[:req, :keyreq].include? @kind
|
19
|
+
end
|
20
|
+
|
21
|
+
def variadic?
|
22
|
+
@kind == :rest
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :name
|
26
|
+
attr_reader :kind
|
27
|
+
|
28
|
+
# The parameter's default value (if any)
|
29
|
+
attr_reader :default_value
|
30
|
+
|
31
|
+
def initialize(kind, name, *default_value)
|
32
|
+
# The parameter list matches the values returned from MethodReflection.parameters,
|
33
|
+
# which has two or three entries: <tt>kind, name [ . default_value ]</tt>
|
34
|
+
|
35
|
+
expect! kind => [:req, :opt, :keyreq, :key, :rest]
|
36
|
+
expect! default_value.length => [0, 1]
|
37
|
+
|
38
|
+
@kind = kind
|
39
|
+
@name = name
|
40
|
+
@default_value = default_value[0]
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Simple::Service
|
2
|
+
# Returns the current context.
|
3
|
+
def self.context
|
4
|
+
Thread.current[:"Simple::Service.context"]
|
5
|
+
end
|
6
|
+
|
7
|
+
# yields a block with a given context, and restores the previous context
|
8
|
+
# object afterwards.
|
9
|
+
def self.with_context(ctx = nil, &block)
|
10
|
+
old_ctx = Thread.current[:"Simple::Service.context"]
|
11
|
+
new_ctx = old_ctx ? old_ctx.merge(ctx) : Context.new(ctx)
|
12
|
+
|
13
|
+
Thread.current[:"Simple::Service.context"] = new_ctx
|
14
|
+
|
15
|
+
block.call
|
16
|
+
ensure
|
17
|
+
Thread.current[:"Simple::Service.context"] = old_ctx
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Simple::Service
|
22
|
+
# A context object
|
23
|
+
#
|
24
|
+
# Each service executes with a current context. The system manages a stack of
|
25
|
+
# contexts; whenever a service execution is done the current context is reverted
|
26
|
+
# to its previous value.
|
27
|
+
#
|
28
|
+
# A context object can store a large number of values; the only way to set or
|
29
|
+
# access a value is via getters and setters. These are implemented via
|
30
|
+
# +method_missing(..)+.
|
31
|
+
#
|
32
|
+
# Also, once a value is set in the context it is not possible to change or
|
33
|
+
# unset it.
|
34
|
+
class Context
|
35
|
+
def initialize(hsh = nil) # @private
|
36
|
+
@hsh = hsh || {}
|
37
|
+
end
|
38
|
+
|
39
|
+
# returns a new Context object, which merges the values in the +overlay+
|
40
|
+
# argument (which must be a Hash or nil) with the values in this context.
|
41
|
+
#
|
42
|
+
# The overlay is allowed to change values in the current context.
|
43
|
+
#
|
44
|
+
# It does not change this context.
|
45
|
+
def merge(overlay)
|
46
|
+
expect! overlay => [Hash, nil]
|
47
|
+
|
48
|
+
overlay ||= {}
|
49
|
+
new_context_hsh = @hsh.merge(overlay)
|
50
|
+
::Simple::Service::Context.new(new_context_hsh)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # @private
|
56
|
+
IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # @private
|
57
|
+
ASSIGNMENT_REGEXP = Regexp.compile("\\A(#{IDENTIFIER_PATTERN})=\\z") # @private
|
58
|
+
|
59
|
+
def method_missing(sym, *args, &block)
|
60
|
+
raise ArgumentError, "Block given" if block
|
61
|
+
|
62
|
+
if args.count == 0 && sym =~ IDENTIFIER_REGEXP
|
63
|
+
self[sym]
|
64
|
+
elsif args.count == 1 && sym =~ ASSIGNMENT_REGEXP
|
65
|
+
self[$1.to_sym] = args.first
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def respond_to_missing?(sym, include_private = false)
|
72
|
+
# :nocov:
|
73
|
+
return true if IDENTIFIER_REGEXP.match?(sym)
|
74
|
+
return true if ASSIGNMENT_REGEXP.match?(sym)
|
75
|
+
|
76
|
+
super
|
77
|
+
# :nocov:
|
78
|
+
end
|
79
|
+
|
80
|
+
def [](key)
|
81
|
+
@hsh[key]
|
82
|
+
end
|
83
|
+
|
84
|
+
def []=(key, value)
|
85
|
+
existing_value = @hsh[key]
|
86
|
+
|
87
|
+
unless existing_value.nil? || existing_value == value
|
88
|
+
raise ::Simple::Service::ContextReadOnlyError, key
|
89
|
+
end
|
90
|
+
|
91
|
+
@hsh[key] = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Simple::Service
|
2
|
+
# Will be raised by ::Simple::Service.action.
|
3
|
+
class NoSuchAction < ::ArgumentError
|
4
|
+
attr_reader :service, :name
|
5
|
+
def initialize(service, name)
|
6
|
+
@service, @name = service, name
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
action_names = ::Simple::Service.actions(service).keys.sort
|
11
|
+
informal = "service #{service} has these actions: #{action_names.map(&:inspect).join(", ")}"
|
12
|
+
"No such action #{name.inspect}; #{informal}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ArgumentError < ::ArgumentError
|
17
|
+
end
|
18
|
+
|
19
|
+
class MissingArguments < ArgumentError
|
20
|
+
attr_reader :action
|
21
|
+
attr_reader :parameters
|
22
|
+
|
23
|
+
def initialize(action, parameters)
|
24
|
+
@action, @parameters = action, parameters
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"#{action}: missing argument(s): #{parameters.map(&:to_s).join(", ")}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class ExtraArguments < ArgumentError
|
33
|
+
attr_reader :action
|
34
|
+
attr_reader :arguments
|
35
|
+
|
36
|
+
def initialize(action, arguments)
|
37
|
+
@action, @arguments = action, arguments
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
str = @arguments.map(&:inspect).join(", ")
|
42
|
+
"#{action}: extra argument(s) #{str}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ContextMissingError < ::StandardError
|
47
|
+
end
|
48
|
+
|
49
|
+
class ContextReadOnlyError < ::StandardError
|
50
|
+
def initialize(key)
|
51
|
+
super "Cannot overwrite existing context setting #{key.inspect}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Simple # @private
|
2
|
+
end
|
3
|
+
|
4
|
+
module Simple::Service
|
5
|
+
module GemHelper # @private
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def version(name)
|
9
|
+
spec = Gem.loaded_specs[name]
|
10
|
+
version = spec ? spec.version.to_s : "0.0.0"
|
11
|
+
version += "+unreleased" if !spec || unreleased?(spec)
|
12
|
+
version
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def unreleased?(spec)
|
18
|
+
return false unless defined?(Bundler::Source::Gemspec)
|
19
|
+
return true if spec.source.is_a?(::Bundler::Source::Gemspec)
|
20
|
+
# :nocov:
|
21
|
+
return true if spec.source.is_a?(::Bundler::Source::Path)
|
22
|
+
|
23
|
+
false
|
24
|
+
# :nocov:
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
VERSION = GemHelper.version "simple-service"
|
29
|
+
end
|
data/log/.gitkeep
ADDED
File without changes
|
data/scripts/release
ADDED
data/scripts/release.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# -- helpers ------------------------------------------------------------------
|
4
|
+
|
5
|
+
def sys(cmd)
|
6
|
+
STDERR.puts "> #{cmd}"
|
7
|
+
system cmd
|
8
|
+
return true if $?.success?
|
9
|
+
|
10
|
+
STDERR.puts "> #{cmd} returned with exitstatus #{$?.exitstatus}"
|
11
|
+
$?.success?
|
12
|
+
end
|
13
|
+
|
14
|
+
def sys!(cmd, error: nil)
|
15
|
+
return true if sys(cmd)
|
16
|
+
STDERR.puts error if error
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def die!(msg)
|
21
|
+
STDERR.puts msg
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
|
25
|
+
ROOT = File.expand_path("#{File.dirname(__FILE__)}/..")
|
26
|
+
|
27
|
+
GEMSPEC = Dir.glob("*.gemspec").first || die!("Missing gemspec file.")
|
28
|
+
|
29
|
+
# -- Version reading and bumping ----------------------------------------------
|
30
|
+
|
31
|
+
module Version
|
32
|
+
extend self
|
33
|
+
|
34
|
+
VERSION_FILE = "#{Dir.getwd}/VERSION"
|
35
|
+
|
36
|
+
def read_version
|
37
|
+
version = File.exist?(VERSION_FILE) ? File.read(VERSION_FILE) : "0.0.1"
|
38
|
+
version.chomp!
|
39
|
+
raise "Invalid version number in #{VERSION_FILE}" unless version =~ /^\d+\.\d+\.\d+$/
|
40
|
+
version
|
41
|
+
end
|
42
|
+
|
43
|
+
def auto_version_bump
|
44
|
+
old_version_number = read_version
|
45
|
+
old = old_version_number.split('.')
|
46
|
+
|
47
|
+
current = old[0..-2] << old[-1].next
|
48
|
+
current.join('.')
|
49
|
+
end
|
50
|
+
|
51
|
+
def bump_version
|
52
|
+
next_version = ENV["VERSION"] || auto_version_bump
|
53
|
+
File.open(VERSION_FILE, "w") { |io| io.write next_version }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# -- check, bump, release a new gem version -----------------------------------
|
58
|
+
|
59
|
+
Dir.chdir ROOT
|
60
|
+
$BASE_BRANCH = ENV['BRANCH'] || 'master'
|
61
|
+
|
62
|
+
# ENV["BUNDLE_GEMFILE"] = "#{Dir.getwd}/Gemfile"
|
63
|
+
# sys! "bundle install"
|
64
|
+
|
65
|
+
sys! "git diff --exit-code > /dev/null", error: 'There are unstaged changes in your working directory'
|
66
|
+
sys! "git diff --cached --exit-code > /dev/null", error: 'There are staged but uncommitted changes'
|
67
|
+
|
68
|
+
sys! "git checkout #{$BASE_BRANCH}"
|
69
|
+
sys! "git pull"
|
70
|
+
|
71
|
+
Version.bump_version
|
72
|
+
version = Version.read_version
|
73
|
+
|
74
|
+
sys! "git add VERSION"
|
75
|
+
sys! "git commit -m \"bump gem to v#{version}\""
|
76
|
+
sys! "git tag -a v#{version} -m \"Tag #{version}\""
|
77
|
+
|
78
|
+
sys! "gem build #{GEMSPEC}"
|
79
|
+
|
80
|
+
sys! "git push origin #{$BASE_BRANCH}"
|
81
|
+
sys! 'git push --tags --force'
|
82
|
+
sys! "gem push #{Dir.glob('*.gem').first}"
|
83
|
+
|
84
|
+
sys! "mkdir -p pkg"
|
85
|
+
sys! "mv *.gem pkg"
|
86
|
+
|
87
|
+
STDERR.puts <<-MSG
|
88
|
+
================================================================================
|
89
|
+
Thank you for releasing a new gem version. You made my day.
|
90
|
+
================================================================================
|
91
|
+
MSG
|