simple-service 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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 +173 -0
- data/lib/simple/service/action.rb +190 -0
- data/lib/simple/service/action/comment.rb +57 -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_invoke3_spec.rb +266 -0
- data/spec/simple/service/action_invoke_spec.rb +166 -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 +156 -0
- data/spec/simple/service/version_spec.rb +7 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/spec_services.rb +58 -0
- metadata +129 -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,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.to_s
|
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
|