simple-service 0.1.1

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 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