as_method 0.1.0

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: e0e9e993c21de8b7a1d579d9228ee8901f7b18cc892f37a0e0b1f9a6ef6a50a7
4
+ data.tar.gz: 2f7506bb9f6ed58793c2b4f606035baffc3fb8f608d88d778047ccd9e769c1fe
5
+ SHA512:
6
+ metadata.gz: b94dd8cc2f9049a71fcbea9bf789ce2794df3d5593b390aa558eeaf7837a12c015c9e8e265898ad1751ccd7effb397e2f43c78eb92d503c628f123747d58f09e
7
+ data.tar.gz: 98c396c3768c0ca040f25d5d103d18fe71b05baec6571ff3da5396c608a63c3fedf60f226d77325dd1c83ef2c3bc1bc55ec1111abd836ddf9aa62583983f6da5
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2022-10-06
4
+
5
+ Initial version
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 V.G.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Includable Service Objects
2
+
3
+ Usage example:
4
+
5
+ ```ruby
6
+ class CreateUser
7
+ include as_method ValidateUserAttributes, name: :validate!
8
+ include as_method Generators::GeneratePassword
9
+ include as_method SaveModel, name: :save
10
+
11
+ def call(name, email)
12
+ @name = name
13
+ @email = email
14
+ user = User.new(attributes)
15
+ validate!(user)
16
+ save(user)
17
+ end
18
+
19
+ private
20
+
21
+ def attributes
22
+ {
23
+ name: @name,
24
+ email: @email,
25
+ password: generate_password(length: 30),
26
+ }
27
+ end
28
+ end
29
+ ```
30
+
31
+ ## Why?
32
+
33
+ This approach allows you to:
34
+ - keep bringing reusable methods in the old-fashioned way via `include`, just like our ancestors did;
35
+ - make methods extracted to SOs look and feel just like regular methods;
36
+ - have all used Service Objects (SOs) listed as explicit dependencies;
37
+
38
+ ## Compatibility
39
+
40
+ Tested on Ruby v2.4 .. v3.1, but it is expected to work on all 2.x versions.
41
+
42
+ ## Installation
43
+
44
+ Add to your `Gemfile`
45
+
46
+ ```ruby
47
+ gem "as_method"
48
+ ```
49
+
50
+ To make `as_method` class method available in all classes, add this line to your application loader:
51
+
52
+ ```ruby
53
+ require "as_method/setup"
54
+ ```
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ module DefineIncludableModule
5
+
6
+ PASS = if RUBY_VERSION < "2.7"
7
+ "*args, &block"
8
+ else
9
+ "..."
10
+ end
11
+
12
+ def self.call(includable_module_name, service_object_class, method_name)
13
+ puts "-- DefineIncludableModule #{includable_module_name}"
14
+ code = <<~RUBY
15
+ module #{includable_module_name}
16
+
17
+ def self.ruby2_keywords(*)
18
+ puts "-- ruby2_keywords"
19
+ end if RUBY_VERSION < "2.7"
20
+
21
+ ruby2_keywords def #{method_name}(*args, &block)
22
+ self.class.registered_service_objects[:"#{method_name}"].call(*args, &block)
23
+ end
24
+
25
+ def self.included(base)
26
+ if base.instance_of?(Module)
27
+ fail TypeError, "Can't be included into Module \#{base.inspect}"
28
+ end
29
+ unless base.included_modules.include?(RegistrationMethods)
30
+ #puts "-- included #{includable_module_name} into \#{base}"
31
+ base.include RegistrationMethods
32
+ end
33
+ base.register_service_object(:"#{method_name}", ::#{service_object_class})
34
+ end
35
+
36
+ def self.extended(_base)
37
+ raise "Do not extend, include!"
38
+ end
39
+ end
40
+ RUBY
41
+ instance_eval code
42
+ end
43
+
44
+ end # ... DefineIncludableModule
45
+ end # ... AsMethod
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ class FindOrDefineIncludableModule
5
+
6
+ def self.call(service_object, method_name)
7
+ new.call(service_object, method_name)
8
+ end
9
+
10
+ def call(service_object, method_name)
11
+ # Note: order matters:
12
+ self.service_object = service_object
13
+ self.method_name = method_name
14
+ self.includable_module_name = generate_includable_module_name(service_object, method_name)
15
+
16
+ define_includable_module unless Object.const_defined?(includable_module_name)
17
+
18
+ Object.const_get(includable_module_name)
19
+ end
20
+
21
+ private
22
+
23
+ attr_accessor :includable_module_name
24
+ attr_reader :method_name
25
+ attr_reader :service_object
26
+
27
+ def define_includable_module
28
+ DefineIncludableModule.call(includable_module_name, service_object, method_name)
29
+ end
30
+
31
+ def generate_method_name_from_so_name
32
+ MethodName::GenerateFromModuleName.call(service_object.name)
33
+ end
34
+
35
+ def generate_includable_module_name(service_object, method_name)
36
+ ModuleName::GenerateForIncludableModule.call(service_object, method_name)
37
+ end
38
+
39
+ def method_name=(name)
40
+ validate_method_name!(name.to_s) if name
41
+
42
+ @method_name = name&.to_s || generate_method_name_from_so_name
43
+ end
44
+
45
+ def service_object=(module_or_class)
46
+ unless module_or_class.respond_to?(:call)
47
+ fail NoMethodError, "Expected #{module_or_class} to respond to #call method"
48
+ end
49
+ unless module_or_class.is_a?(Module)
50
+ fail TypeError, "#{module_or_class} must be a Class or a Module"
51
+ end
52
+ @service_object = module_or_class
53
+ end
54
+
55
+ def validate_method_name!(name)
56
+ return if MethodName::ValidateMethodName.call(name)
57
+
58
+ fail NameError, "wrong method name #{name.inspect}"
59
+ end
60
+
61
+ end # ... FindOrDefineIncludableModule
62
+ end # ... AsMethod
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ module MethodName
5
+ module GenerateFromModuleName
6
+
7
+ def self.call(module_name)
8
+ ModuleName::StripNamespace.call(module_name)
9
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
10
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
11
+ .downcase
12
+ end
13
+
14
+ end # ... GenerateFromModuleName
15
+ end # ... MethodName
16
+ end # ... AsMethod
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ module MethodName
5
+ module ValidateMethodName
6
+
7
+ REGULAR_NAME_REGEX = /\A[a-z_]+[a-z0-9_]*[?!=]{0,1}\z/
8
+
9
+ SPECIAL_NAMES = %w{[] ! ~ + ** - * / % << >> & | ^ < <= >= > == === != =~ !~ <=>}
10
+
11
+ def self.call(name)
12
+ return true if name.to_s =~ REGULAR_NAME_REGEX
13
+ return true if SPECIAL_NAMES.include?(name.to_s)
14
+
15
+ false
16
+ end
17
+
18
+ end # ... ValidateMethodName
19
+ end # ... MethodName
20
+ end # ... AsMethod
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ module ModuleName
5
+ module GenerateForIncludableModule
6
+
7
+ def self.call(module_or_class, method_name = nil)
8
+ part = GenerateFromUnderscored.call(method_name) || StripNamespace.call(module_or_class)
9
+
10
+ "::#{module_or_class}::As#{part}Method"
11
+ end
12
+
13
+ end # ... GenerateForIncludableModule
14
+ end # ... ModuleName
15
+ end # ... AsMethod
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ module ModuleName
5
+ module GenerateFromUnderscored
6
+
7
+ REPLACEMENTS = {
8
+ "-" => "_Min",
9
+ "!" => "_Exc",
10
+ "?" => "_Que",
11
+ "[" => "_Sbl",
12
+ "]" => "_Sbr",
13
+ "*" => "_Ast",
14
+ "/" => "_Sla",
15
+ "&" => "_Amp",
16
+ "%" => "_Pct",
17
+ "^" => "_Pwr",
18
+ "+" => "_Plu",
19
+ "<" => "_Lst",
20
+ "=" => "_Eql",
21
+ ">" => "_Gtt",
22
+ "|" => "_Vbr",
23
+ "~" => "_Til",
24
+ }.freeze
25
+
26
+ def self.call(string)
27
+ return unless string
28
+
29
+ string.to_s
30
+ .gsub(/(?:_|(^))([a-z\d]{1})/) { "#{$1}#{$2.capitalize}" }
31
+ .gsub(/[#{Regexp.escape(REPLACEMENTS.keys.join)}]/, REPLACEMENTS)
32
+ end
33
+
34
+ end # ... GenerateFromUnderscored
35
+ end # ... ModuleName
36
+ end # ... AsMethod
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ module ModuleName
5
+ module StripNamespace
6
+
7
+ def self.call(module_or_class)
8
+ module_or_class.to_s.match(/[^:]+\z/)&.to_s
9
+ end
10
+
11
+ end # ... StripNamespace
12
+ end # ... ModuleName
13
+ end # ... AsMethod
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ module RegistrationMethods
5
+
6
+ module ClassMethods
7
+ def registered_service_objects
8
+ @registered_service_objects ||= {}
9
+ end
10
+
11
+ def register_service_objects(hash)
12
+ hash.each { |name, so| register_service_object(name, so) }
13
+ end
14
+
15
+ def register_service_object(name, so)
16
+ registered_service_objects[name.to_sym]&.tap do |registered_so|
17
+ return if so == registered_so
18
+ raise "#{so} clashes with #{registered_so} in #{self}" if so != registered_so
19
+ end
20
+
21
+ # puts "--- register #{self.name}##{name} => #{so}#call"
22
+ registered_service_objects[name.to_sym] = so
23
+ end
24
+ end # ... ClassMethods
25
+
26
+ def self.included(base)
27
+ return if base.singleton_class.included_modules.include?(ClassMethods)
28
+
29
+ puts "-- #{base.name} extend with RegistrationMethods::ClassMethods"
30
+ base.extend ClassMethods
31
+ end
32
+
33
+ end # ... RegistrationMethods
34
+ end # ... AsMethod
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage:
4
+ #
5
+ # Require this file to add #as_method class method to all classes:
6
+ # require 'as_method/setup'
7
+ #
8
+
9
+ unless Object.singleton_class.included_modules.include?(AsMethod)
10
+ Object.extend AsMethod
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+
5
+ VERSION = "0.1.0"
6
+
7
+ end
data/lib/as_method.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsMethod
4
+ autoload :DefineIncludableModule, "as_method/define_includable_module"
5
+ autoload :FindOrDefineIncludableModule, "as_method/find_or_define_includable_module"
6
+ autoload :RegistrationMethods, "as_method/registration_methods"
7
+ autoload :VERSION, "as_method/version"
8
+
9
+ module ModuleName
10
+ autoload :GenerateForIncludableModule, "as_method/module_name/generate_for_includable_module"
11
+ autoload :GenerateFromUnderscored, "as_method/module_name/generate_from_underscored"
12
+ autoload :StripNamespace, "as_method/module_name/strip_namespace"
13
+ end
14
+
15
+ module MethodName
16
+ autoload :ValidateMethodName, "as_method/method_name/validate_method_name"
17
+ autoload :GenerateFromModuleName, "as_method/method_name/generate_from_module_name"
18
+ end
19
+
20
+ def as_method(service_object, name: nil)
21
+ includable_module = FindOrDefineIncludableModule.call(service_object, name)
22
+ unless self.included_modules.include?(includable_module)
23
+ includable_module
24
+ else
25
+ Kernel # a bit faster
26
+ end
27
+
28
+ end
29
+ end # ... AsMethod
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: as_method
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vladimir Gorodulin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.14'
41
+ description: Make callable Service Objects includable as methods
42
+ email:
43
+ - ru.hostmaster@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - LICENSE
50
+ - README.md
51
+ - lib/as_method.rb
52
+ - lib/as_method/define_includable_module.rb
53
+ - lib/as_method/find_or_define_includable_module.rb
54
+ - lib/as_method/method_name/generate_from_module_name.rb
55
+ - lib/as_method/method_name/validate_method_name.rb
56
+ - lib/as_method/module_name/generate_for_includable_module.rb
57
+ - lib/as_method/module_name/generate_from_underscored.rb
58
+ - lib/as_method/module_name/strip_namespace.rb
59
+ - lib/as_method/registration_methods.rb
60
+ - lib/as_method/setup.rb
61
+ - lib/as_method/version.rb
62
+ homepage: https://github.com/gorodulin/as_method
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ changelog_uri: https://github.com/gorodulin/as_method/CHANGELOG.md
67
+ homepage_uri: https://github.com/gorodulin/as_method
68
+ source_code_uri: https://github.com/gorodulin/as_method
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 2.7.0
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.1.6
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Call Service Objects with ease. Include them to your classes as methods!
88
+ test_files: []