simple-service 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9641586855a538c0b450d4731d39d28f23c4ee19abec379f5ac9047888fe9106
4
+ data.tar.gz: a395f0a7815935557163cc81532ef7e3fc2b97dfc2bf7347e32460481589dd1d
5
+ SHA512:
6
+ metadata.gz: 43a8d60d9c7b1f14913f431b24ebd50db6a41c2b951a41bd8b1c136d466b2d198b0582e7adcbc34b0df0c81206abe79fda05ea25f8e9c74c4f52a5722e23146a
7
+ data.tar.gz: a0376b41661076f53a4ffe6a20650efc6dda513f762ddd0e96d7c7da0f568b6dc2e687c4fbfb67da759f7ac076ce115468a0fd2ba63a686f7ab15504a1ee5cd8
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ coverage
2
+ rdoc
3
+ pkg
4
+ log/*.log
5
+ .rspec.data
6
+ Gemfile.lock
7
+ .rake_t_cache
8
+ .rspec.status
9
+ .DS_Store
10
+ tmp
11
+ .byebug_history
12
+ .bundle/config
data/.rubocop.yml ADDED
@@ -0,0 +1,96 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+ Exclude:
4
+ - 'spec/**/*'
5
+ - 'test/**/*'
6
+ - 'bin/**/*'
7
+ - 'tasks/release.rake'
8
+ - '*.gemspec'
9
+ - 'Gemfile'
10
+ - 'Rakefile'
11
+ - 'scripts/*.rb'
12
+
13
+ Metrics/LineLength:
14
+ Max: 140
15
+
16
+ Metrics/MethodLength:
17
+ Max: 20
18
+
19
+ Style/SpecialGlobalVars:
20
+ Enabled: false
21
+
22
+ Style/StringLiterals:
23
+ EnforcedStyle: double_quotes
24
+ ConsistentQuotesInMultiline: false
25
+
26
+ Style/ClassAndModuleChildren:
27
+ Enabled: false
28
+
29
+ Style/ModuleFunction:
30
+ Enabled: false
31
+
32
+ Style/FrozenStringLiteralComment:
33
+ Enabled: false
34
+
35
+ Style/Documentation:
36
+ Enabled: false
37
+
38
+ Style/MutableConstant:
39
+ Enabled: false
40
+
41
+ Style/FormatStringToken:
42
+ Enabled: false
43
+
44
+ Style/Lambda:
45
+ Enabled: false
46
+
47
+ Style/SymbolArray:
48
+ Enabled: false
49
+
50
+ Style/FormatString:
51
+ Enabled: false
52
+
53
+ Style/PercentLiteralDelimiters:
54
+ Enabled: false
55
+
56
+ Lint/MissingCopEnableDirective:
57
+ Enabled: false
58
+
59
+ Style/NumericPredicate:
60
+ Enabled: false
61
+
62
+ Style/RegexpLiteral:
63
+ Enabled: false
64
+
65
+ Style/ClassVars:
66
+ Enabled: false
67
+
68
+ Style/ConditionalAssignment:
69
+ Enabled: false
70
+
71
+ Style/IfUnlessModifier:
72
+ Enabled: false
73
+
74
+ Style/PerlBackrefs:
75
+ Enabled: false
76
+
77
+ Style/TrailingUnderscoreVariable:
78
+ Enabled: false
79
+
80
+ Style/StderrPuts:
81
+ Enabled: false
82
+
83
+ Style/NonNilCheck:
84
+ Enabled: false
85
+
86
+ Metrics/ParameterLists:
87
+ Enabled: false
88
+
89
+ Style/StringLiteralsInInterpolation:
90
+ Enabled: false
91
+
92
+ Style/DoubleNegation:
93
+ Enabled: false
94
+
95
+ Style/ParallelAssignment:
96
+ Enabled: false
data/.tm_properties ADDED
@@ -0,0 +1 @@
1
+ excludeDirectories = "{_build,coverage,assets/node_modules,node_modules,deps,db,cover,priv/static,storage,github,vendor,arena,}"
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in {gemname}.gemspec
4
+ gemspec
5
+
6
+ # --- Development and test dependencies ------------------------------
7
+
8
+ group :development, :test do
9
+ gem 'rake', '~> 11'
10
+ gem 'rspec', '~> 3.7'
11
+ # gem 'rubocop', '~> 0.61.1'
12
+ gem 'simplecov', '~> 0'
13
+ gem 'byebug'
14
+ end
data/Makefile ADDED
@@ -0,0 +1,4 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ rspec
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # simple-service – a pretty simple and somewhat abstract service description
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ Dir.glob("tasks/*.rake").each { |r| import r }
2
+
3
+ task :release do
4
+ sh "scripts/release"
5
+ end
6
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
data/bin/bundle ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "rubygems"
12
+
13
+ m = Module.new do
14
+ module_function
15
+
16
+ def invoked_as_script?
17
+ File.expand_path($0) == File.expand_path(__FILE__)
18
+ end
19
+
20
+ def env_var_version
21
+ ENV["BUNDLER_VERSION"]
22
+ end
23
+
24
+ def cli_arg_version
25
+ return unless invoked_as_script? # don't want to hijack other binstubs
26
+ return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27
+ bundler_version = nil
28
+ update_index = nil
29
+ ARGV.each_with_index do |a, i|
30
+ if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31
+ bundler_version = a
32
+ end
33
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34
+ bundler_version = $1 || ">= 0.a"
35
+ update_index = i
36
+ end
37
+ bundler_version
38
+ end
39
+
40
+ def gemfile
41
+ gemfile = ENV["BUNDLE_GEMFILE"]
42
+ return gemfile if gemfile && !gemfile.empty?
43
+
44
+ File.expand_path("../../Gemfile", __FILE__)
45
+ end
46
+
47
+ def lockfile
48
+ lockfile =
49
+ case File.basename(gemfile)
50
+ when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51
+ else "#{gemfile}.lock"
52
+ end
53
+ File.expand_path(lockfile)
54
+ end
55
+
56
+ def lockfile_version
57
+ return unless File.file?(lockfile)
58
+ lockfile_contents = File.read(lockfile)
59
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60
+ Regexp.last_match(1)
61
+ end
62
+
63
+ def bundler_version
64
+ @bundler_version ||= begin
65
+ env_var_version || cli_arg_version ||
66
+ lockfile_version || "#{Gem::Requirement.default}.a"
67
+ end
68
+ end
69
+
70
+ def load_bundler!
71
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
72
+
73
+ # must dup string for RG < 1.8 compatibility
74
+ activate_bundler(bundler_version.dup)
75
+ end
76
+
77
+ def activate_bundler(bundler_version)
78
+ if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0")
79
+ bundler_version = "< 2"
80
+ end
81
+ gem_error = activation_error_handling do
82
+ gem "bundler", bundler_version
83
+ end
84
+ return if gem_error.nil?
85
+ require_error = activation_error_handling do
86
+ require "bundler/version"
87
+ end
88
+ return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION))
89
+ warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`"
90
+ exit 42
91
+ end
92
+
93
+ def activation_error_handling
94
+ yield
95
+ nil
96
+ rescue StandardError, LoadError => e
97
+ e
98
+ end
99
+ end
100
+
101
+ m.load_bundler!
102
+
103
+ if m.invoked_as_script?
104
+ load Gem.bin_path("bundler", "bundle")
105
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ Bundler.require
5
+ require "simple-service"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,57 @@
1
+ # returns the comment for an action
2
+ class ::Simple::Service::Action::Comment
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 # :nodoc:
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 keyword 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 keyword 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,46 @@
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
+ [:key, :keyreq].include? @kind
11
+ end
12
+
13
+ def anonymous?
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
+ def optional?
26
+ !required?
27
+ end
28
+
29
+ attr_reader :name
30
+ attr_reader :kind
31
+
32
+ # The parameter's default value (if any)
33
+ attr_reader :default_value
34
+
35
+ def initialize(kind, name, *default_value)
36
+ # The parameter list matches the values returned from MethodReflection.parameters,
37
+ # which has two or three entries: <tt>kind, name [ . default_value ]</tt>
38
+
39
+ expect! kind => [:req, :opt, :keyreq, :key, :rest]
40
+ expect! default_value.length => [0, 1]
41
+
42
+ @kind = kind
43
+ @name = name
44
+ @default_value = default_value[0]
45
+ end
46
+ end
@@ -0,0 +1,193 @@
1
+ # rubocop:disable Metrics/CyclomaticComplexity
2
+ # rubocop:disable Metrics/AbcSize
3
+ # rubocop:disable Metrics/MethodLength
4
+ # rubocop:disable Metrics/PerceivedComplexity
5
+
6
+ module Simple::Service
7
+ class Action
8
+ end
9
+ end
10
+
11
+ require_relative "./action/comment"
12
+ require_relative "./action/parameter"
13
+
14
+ module Simple::Service
15
+ class Action
16
+ ArgumentError = ::Simple::Service::ArgumentError
17
+
18
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*"
19
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z")
20
+
21
+ # determines all services provided by the +service+ service module.
22
+ def self.enumerate(service:) # :nodoc:
23
+ service.public_instance_methods(false)
24
+ .grep(IDENTIFIER_REGEXP)
25
+ .each_with_object({}) { |name, hsh| hsh[name] = Action.new(service, name) }
26
+ end
27
+
28
+ attr_reader :service
29
+ attr_reader :name
30
+
31
+ # returns an Array of Parameter structures.
32
+ def parameters
33
+ @parameters ||= Parameter.reflect_on_method(service: service, name: name)
34
+ end
35
+
36
+ def initialize(service, name)
37
+ @service = service
38
+ @name = name
39
+
40
+ parameters
41
+ end
42
+
43
+ def short_description
44
+ comment.short
45
+ end
46
+
47
+ def full_description
48
+ comment.full
49
+ end
50
+
51
+ private
52
+
53
+ # returns a Comment object
54
+ #
55
+ # The comment object is extracted on demand on the first call.
56
+ def comment
57
+ @comment ||= Comment.extract(action: self)
58
+ end
59
+
60
+ public
61
+
62
+ def source_location
63
+ @service.instance_method(name).source_location
64
+ end
65
+
66
+ # build a service_instance and run the action, with arguments constructed from
67
+ # args_hsh and params_hsh.
68
+ def invoke(args, options)
69
+ args ||= {}
70
+ options ||= {}
71
+
72
+ # convert Array arguments into a Hash of named arguments. This is strictly
73
+ # necessary to be able to apply default value-based type conversions. (On
74
+ # the downside this also means we convert an array to a hash and then back
75
+ # into an array. This, however, should only be an issue for CLI based action
76
+ # invocations, because any other use case (that I can think of) should allow
77
+ # us to provide arguments as a Hash.
78
+ if args.is_a?(Array)
79
+ args = convert_argument_array_to_hash(args)
80
+ end
81
+
82
+ # [TODO] Type conversion according to default values.
83
+ args_ary = build_method_arguments(args, options)
84
+
85
+ service_instance = Object.new
86
+ service_instance.extend service
87
+ service_instance.public_send(@name, *args_ary)
88
+ end
89
+
90
+ private
91
+
92
+ module IndifferentHashEx
93
+ def self.fetch(hsh, name)
94
+ missing_key!(name) unless hsh
95
+
96
+ hsh.fetch(name.to_sym) do
97
+ hsh.fetch(name.to_s) do
98
+ missing_key!(name)
99
+ end
100
+ end
101
+ end
102
+
103
+ def self.key?(hsh, name)
104
+ return false unless hsh
105
+
106
+ hsh.key?(name.to_sym) || hsh.key?(name.to_s)
107
+ end
108
+
109
+ def self.missing_key!(name)
110
+ raise ArgumentError, "Missing argument in arguments hash: #{name}"
111
+ end
112
+ end
113
+
114
+ I = IndifferentHashEx
115
+
116
+ # returns an array of arguments suitable to be sent to the action method.
117
+ def build_method_arguments(args_hsh, params_hsh)
118
+ args = []
119
+ keyword_args = {}
120
+
121
+ parameters.each do |parameter|
122
+ if parameter.keyword?
123
+ if I.key?(params_hsh, parameter.name)
124
+ keyword_args[parameter.name] = I.fetch(params_hsh, parameter.name)
125
+ end
126
+ else
127
+ if parameter.variadic?
128
+ if I.key?(args_hsh, parameter.name)
129
+ args.concat(Array(I.fetch(args_hsh, parameter.name)))
130
+ end
131
+ else
132
+ if !parameter.optional? || I.key?(args_hsh, parameter.name)
133
+ args << I.fetch(args_hsh, parameter.name)
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ unless keyword_args.empty?
140
+ args << keyword_args
141
+ end
142
+
143
+ args
144
+ end
145
+
146
+ def convert_argument_array_to_hash(ary)
147
+ # enumerate all of the action's anonymous arguments, trying to match them
148
+ # against the values in +ary+. If afterwards any arguments are still left
149
+ # in +ary+ they will be assigned to the variadic arguments array, which
150
+ # - if a variadic parameter is defined in this action - will be added to
151
+ # the hash as well.
152
+ hsh = {}
153
+ variadic_parameter_name = nil
154
+
155
+ parameters.each do |parameter|
156
+ next if parameter.keyword?
157
+ parameter_name = parameter.name
158
+
159
+ if parameter.variadic?
160
+ variadic_parameter_name = parameter_name
161
+ next
162
+ end
163
+
164
+ if ary.empty? && !parameter.optional?
165
+ raise ::Simple::Service::ArgumentError, "Missing #{parameter_name} parameter"
166
+ end
167
+
168
+ next if ary.empty?
169
+
170
+ hsh[parameter_name] = ary.shift
171
+ end
172
+
173
+ # Any arguments are left? Set variadic parameter, if defined, raise an error otherwise.
174
+ unless ary.empty?
175
+ unless variadic_parameter_name
176
+ raise ::Simple::Service::ArgumentError, "Extra parameters: #{ary.map(&:inspect).join(", ")}"
177
+ end
178
+
179
+ hsh[variadic_parameter_name] = ary
180
+ end
181
+
182
+ hsh
183
+ end
184
+
185
+ def full_name
186
+ "#{service}##{name}"
187
+ end
188
+
189
+ def to_s
190
+ full_name
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,52 @@
1
+ module Simple::Service
2
+ class Context
3
+ class ReadOnlyError < RuntimeError
4
+ def initialize(key)
5
+ super "Cannot overwrite existing context setting #{key.inspect}"
6
+ end
7
+ end
8
+
9
+ def initialize
10
+ @hsh = {}
11
+ end
12
+
13
+ private
14
+
15
+ def [](key)
16
+ @hsh[key]
17
+ end
18
+
19
+ def []=(key, value)
20
+ existing_value = @hsh[key]
21
+
22
+ unless existing_value.nil? || existing_value == value
23
+ raise ReadOnlyError, key
24
+ end
25
+
26
+ @hsh[key] = value
27
+ end
28
+
29
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*"
30
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z")
31
+ ASSIGNMENT_REGEXP = Regexp.compile("\\A(#{IDENTIFIER_PATTERN})=\\z")
32
+
33
+ def method_missing(sym, *args, &block)
34
+ if block
35
+ super
36
+ elsif args.count == 0 && sym =~ IDENTIFIER_REGEXP
37
+ self[sym]
38
+ elsif args.count == 1 && sym =~ ASSIGNMENT_REGEXP
39
+ self[$1.to_sym] = args.first
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ def respond_to_missing?(sym, include_private = false)
46
+ return true if IDENTIFIER_REGEXP.maptch?(sym)
47
+ return true if ASSIGNMENT_REGEXP.maptch?(sym)
48
+
49
+ super
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ class Simple::Service
2
+ module GemHelper
3
+ extend self
4
+
5
+ def version(name)
6
+ spec = Gem.loaded_specs[name]
7
+ version = spec ? spec.version.to_s : "0.0.0"
8
+ version += "+unreleased" if !spec || unreleased?(spec)
9
+ version
10
+ end
11
+
12
+ private
13
+
14
+ def unreleased?(spec)
15
+ return false unless defined?(Bundler::Source::Gemspec)
16
+ return true if spec.source.is_a?(::Bundler::Source::Gemspec)
17
+ return true if spec.source.is_a?(::Bundler::Source::Path)
18
+
19
+ false
20
+ end
21
+ end
22
+
23
+ VERSION = GemHelper.version "simple-service"
24
+ end
@@ -0,0 +1,89 @@
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
+ # The Simple::Service module.
10
+ #
11
+ # To mark a target module as a service module one must include the
12
+ # Simple::Service module into the target module.
13
+ #
14
+ # This serves as a marker that this module is actually intended
15
+ # to be used as a service.
16
+ module Simple::Service
17
+ def self.included(klass)
18
+ klass.extend ClassMethods
19
+ end
20
+
21
+ # Returns the current context.
22
+ def self.context
23
+ Thread.current[:"Simple::Service.context"]
24
+ end
25
+
26
+ # yields a block with a given context, and restores the previous context
27
+ # object afterwards.
28
+ def self.with_context(ctx, &block)
29
+ expect! ctx => [Simple::Service::Context, nil]
30
+ _ = block
31
+
32
+ old_ctx = Thread.current[:"Simple::Service.context"]
33
+ Thread.current[:"Simple::Service.context"] = ctx
34
+ yield
35
+ ensure
36
+ Thread.current[:"Simple::Service.context"] = old_ctx
37
+ end
38
+
39
+ def self.action(service, name)
40
+ actions = self.actions(service)
41
+ actions[name] || begin
42
+ action_names = actions.keys.sort
43
+ informal = "service #{service} has these actions: #{action_names.map(&:inspect).join(", ")}"
44
+ raise "No such action #{name.inspect}; #{informal}"
45
+ end
46
+ end
47
+
48
+ def self.service?(service)
49
+ service.is_a?(Module) && service.include?(self)
50
+ end
51
+
52
+ def self.actions(service)
53
+ raise ArgumentError, "service must be a #{self}" unless service?(service)
54
+
55
+ service.__simple_service_actions__
56
+ end
57
+
58
+ def self.invoke(service, name, arguments, params, context: nil)
59
+ with_context(context) do
60
+ action(service, name).invoke(arguments, params)
61
+ end
62
+ end
63
+
64
+ module ClassMethods
65
+ # returns a Hash of actions provided by the service module.
66
+ def __simple_service_actions__ # :nodoc:
67
+ @__simple_service_actions__ ||= Action.enumerate(service: self)
68
+ end
69
+ end
70
+
71
+ # Resolves a service by name. Returns nil if the name does not refer to a service,
72
+ # or the service module otherwise.
73
+ def self.resolve(str)
74
+ return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
75
+
76
+ service = resolve_constant(str)
77
+
78
+ return unless service.is_a?(Module)
79
+ return unless service.include?(::Simple::Service)
80
+
81
+ service
82
+ end
83
+
84
+ def self.resolve_constant(str)
85
+ const_get(str)
86
+ rescue NameError
87
+ nil
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ # rubocop:disable Naming/FileName
2
+
3
+ require "simple/service"
data/lib/simple.rb ADDED
@@ -0,0 +1,2 @@
1
+ module Simple
2
+ end
data/log/.gitkeep ADDED
File without changes
data/scripts/release ADDED
@@ -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
data/scripts/stats ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+
3
+ cloc $(find lib/ -name *rb) | grep -E 'Language|Ruby' | sed 's-Language- -'
4
+ printf "\n"
data/scripts/watch ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ watchr lib,spec rspec $@
@@ -0,0 +1,25 @@
1
+ # lib = File.expand_path('../lib', __FILE__)
2
+ # $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "simple-service"
6
+ gem.version = File.read("VERSION")
7
+
8
+ gem.authors = [ "radiospiel" ]
9
+ gem.email = "eno@radiospiel.org"
10
+ gem.homepage = "http://github.com/radiospiel/simple-service"
11
+ gem.summary = "Pretty simple and somewhat abstract service description"
12
+
13
+ gem.description = "Pretty simple and somewhat abstract service description"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = %w(lib)
18
+
19
+ # executables are used for development purposes only
20
+ gem.executables = []
21
+
22
+ gem.required_ruby_version = '~> 2.5'
23
+
24
+ gem.add_dependency "expectation", "~> 1"
25
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe Simple::Service::Context do
4
+ let(:context) { Simple::Service::Context.new }
5
+
6
+ before do
7
+ context.one = 1
8
+ end
9
+
10
+ describe "reading" do
11
+ it "returns a value if set" do
12
+ expect(context.one).to eq(1)
13
+ end
14
+
15
+ it "returns nil if not set" do
16
+ expect(context.two).to be_nil
17
+ end
18
+ end
19
+
20
+ describe "writing" do
21
+ it "sets a value if it does not exist yet" do
22
+ context.two = 2
23
+ expect(context.two).to eq(2)
24
+ end
25
+
26
+ it "raises a ReadOnly error if the value exists and is not equal" do
27
+ expect {
28
+ context.one = 2
29
+ }.to raise_error(::Simple::Service::Context::ReadOnlyError)
30
+ end
31
+
32
+ it "sets the value if it exists and is equal" do
33
+ context.one = 1
34
+ expect(context.one).to eq(1)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ ENV["RACK_ENV"] = "test"
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require "byebug"
5
+ require "rspec"
6
+
7
+ Dir.glob("./spec/support/**/*.rb").sort.each { |path| load path }
8
+
9
+ require "simple/service"
10
+
11
+ RSpec.configure do |config|
12
+ config.run_all_when_everything_filtered = true
13
+ config.filter_run focus: (ENV["CI"] != "true")
14
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
15
+ config.order = "random"
16
+ config.example_status_persistence_file_path = ".rspec.data"
17
+
18
+ config.backtrace_exclusion_patterns << /spec\/support/
19
+ config.backtrace_exclusion_patterns << /spec_helper/
20
+ config.backtrace_exclusion_patterns << /database_cleaner/
21
+
22
+ # config.around(:each) do |example|
23
+ # example.run
24
+ # end
25
+ end
@@ -0,0 +1,13 @@
1
+ require "simplecov"
2
+
3
+ SimpleCov.start do
4
+ # return true to remove src from coverage
5
+ add_filter do |src|
6
+ next true if src.filename =~ /\/spec\//
7
+ next true if src.filename =~ /\/version.rb$/
8
+
9
+ false
10
+ end
11
+
12
+ # minimum_coverage 90
13
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple-service
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - radiospiel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: expectation
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ description: Pretty simple and somewhat abstract service description
28
+ email: eno@radiospiel.org
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".gitignore"
34
+ - ".rubocop.yml"
35
+ - ".tm_properties"
36
+ - Gemfile
37
+ - Makefile
38
+ - README.md
39
+ - Rakefile
40
+ - VERSION
41
+ - bin/bundle
42
+ - bin/console
43
+ - bin/rake
44
+ - bin/rspec
45
+ - lib/simple-service.rb
46
+ - lib/simple.rb
47
+ - lib/simple/service.rb
48
+ - lib/simple/service/action.rb
49
+ - lib/simple/service/action/comment.rb
50
+ - lib/simple/service/action/method_reflection.rb
51
+ - lib/simple/service/action/parameter.rb
52
+ - lib/simple/service/context.rb
53
+ - lib/simple/service/version.rb
54
+ - log/.gitkeep
55
+ - scripts/release
56
+ - scripts/release.rb
57
+ - scripts/stats
58
+ - scripts/watch
59
+ - simple-service.gemspec
60
+ - spec/simple/service/context_spec.rb
61
+ - spec/spec_helper.rb
62
+ - spec/support/004_simplecov.rb
63
+ homepage: http://github.com/radiospiel/simple-service
64
+ licenses: []
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.5'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.0.4
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Pretty simple and somewhat abstract service description
85
+ test_files:
86
+ - spec/simple/service/context_spec.rb
87
+ - spec/spec_helper.rb
88
+ - spec/support/004_simplecov.rb