simple-service 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) 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 +173 -0
  47. data/lib/simple/service/action.rb +190 -0
  48. data/lib/simple/service/action/comment.rb +57 -0
  49. data/lib/simple/service/action/method_reflection.rb +70 -0
  50. data/lib/simple/service/action/parameter.rb +42 -0
  51. data/lib/simple/service/context.rb +94 -0
  52. data/lib/simple/service/errors.rb +54 -0
  53. data/lib/simple/service/version.rb +29 -0
  54. data/log/.gitkeep +0 -0
  55. data/scripts/release +2 -0
  56. data/scripts/release.rb +91 -0
  57. data/scripts/stats +5 -0
  58. data/scripts/watch +2 -0
  59. data/simple-service.gemspec +25 -0
  60. data/spec/simple/service/action_invoke3_spec.rb +266 -0
  61. data/spec/simple/service/action_invoke_spec.rb +166 -0
  62. data/spec/simple/service/action_spec.rb +51 -0
  63. data/spec/simple/service/context_spec.rb +69 -0
  64. data/spec/simple/service/service_spec.rb +156 -0
  65. data/spec/simple/service/version_spec.rb +7 -0
  66. data/spec/spec_helper.rb +38 -0
  67. data/spec/support/spec_services.rb +58 -0
  68. 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
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