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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rubocop.yml +100 -0
  4. data/.tm_properties +1 -0
  5. data/Gemfile +14 -0
  6. data/Makefile +9 -0
  7. data/README.md +68 -0
  8. data/Rakefile +6 -0
  9. data/VERSION +1 -0
  10. data/bin/bundle +105 -0
  11. data/bin/console +15 -0
  12. data/bin/rake +29 -0
  13. data/bin/rspec +29 -0
  14. data/doc/Simple.html +117 -0
  15. data/doc/Simple/Service.html +863 -0
  16. data/doc/Simple/Service/Action.html +1014 -0
  17. data/doc/Simple/Service/Action/Comment.html +451 -0
  18. data/doc/Simple/Service/Action/Comment/Extractor.html +347 -0
  19. data/doc/Simple/Service/Action/IndieHash.html +506 -0
  20. data/doc/Simple/Service/Action/MethodReflection.html +285 -0
  21. data/doc/Simple/Service/Action/Parameter.html +816 -0
  22. data/doc/Simple/Service/ArgumentError.html +128 -0
  23. data/doc/Simple/Service/ClassMethods.html +187 -0
  24. data/doc/Simple/Service/Context.html +379 -0
  25. data/doc/Simple/Service/ContextMissingError.html +124 -0
  26. data/doc/Simple/Service/ContextReadOnlyError.html +206 -0
  27. data/doc/Simple/Service/ExtraArguments.html +428 -0
  28. data/doc/Simple/Service/GemHelper.html +190 -0
  29. data/doc/Simple/Service/MissingArguments.html +426 -0
  30. data/doc/Simple/Service/NoSuchAction.html +433 -0
  31. data/doc/_index.html +286 -0
  32. data/doc/class_list.html +51 -0
  33. data/doc/css/common.css +1 -0
  34. data/doc/css/full_list.css +58 -0
  35. data/doc/css/style.css +496 -0
  36. data/doc/file.README.html +146 -0
  37. data/doc/file_list.html +56 -0
  38. data/doc/frames.html +17 -0
  39. data/doc/index.html +146 -0
  40. data/doc/js/app.js +303 -0
  41. data/doc/js/full_list.js +216 -0
  42. data/doc/js/jquery.js +4 -0
  43. data/doc/method_list.html +539 -0
  44. data/doc/top-level-namespace.html +110 -0
  45. data/lib/simple-service.rb +3 -0
  46. data/lib/simple/service.rb +143 -0
  47. data/lib/simple/service/action.rb +203 -0
  48. data/lib/simple/service/action/comment.rb +57 -0
  49. data/lib/simple/service/action/indie_hash.rb +37 -0
  50. data/lib/simple/service/action/method_reflection.rb +70 -0
  51. data/lib/simple/service/action/parameter.rb +42 -0
  52. data/lib/simple/service/context.rb +94 -0
  53. data/lib/simple/service/errors.rb +54 -0
  54. data/lib/simple/service/version.rb +29 -0
  55. data/log/.gitkeep +0 -0
  56. data/scripts/release +2 -0
  57. data/scripts/release.rb +91 -0
  58. data/scripts/stats +5 -0
  59. data/scripts/watch +2 -0
  60. data/simple-service.gemspec +25 -0
  61. data/spec/simple/service/action_invoke2_spec.rb +166 -0
  62. data/spec/simple/service/action_invoke_spec.rb +266 -0
  63. data/spec/simple/service/action_spec.rb +51 -0
  64. data/spec/simple/service/context_spec.rb +69 -0
  65. data/spec/simple/service/service_spec.rb +105 -0
  66. data/spec/simple/service/version_spec.rb +7 -0
  67. data/spec/spec_helper.rb +38 -0
  68. data/spec/support/spec_services.rb +50 -0
  69. 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
File without changes
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ $0.rb "$@"
@@ -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