zen-service 2.0.0 → 2.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64770c881ffd916d2ae348ea1e0e1b2f50f0ae1ddfe77e4cb33488894800a241
4
- data.tar.gz: 1cf89ac8571af9af101ddfe2458a0a09e371a40e6d1e6bab9a58be0c0eec1445
3
+ metadata.gz: 12dcff33c206c08f710f8cedf505f663387054b22aa6429556b20c47eac6b410
4
+ data.tar.gz: b314175b5a9a5634480ac0c4fbec0777fa895e68687eedbaad8382f1b26600fe
5
5
  SHA512:
6
- metadata.gz: 6467ffcd8497764c8974f45cef7106865d3109c3a794a2b7cbefd6342a3d69599392f56a83386be728ecca753fdf247a554ebd8b20c726ccc723b767618f54eb
7
- data.tar.gz: '034914344b92311d62df7eec453791f87b39111a1d6f29c5344761718468c6f103a012a8981a37b0e358f612ab581ec29d4bb2a65f2b86f3a1f88ce303628157'
6
+ metadata.gz: 3bb64af64bf4641e01b686bf4b8bda93e5efcd8188d1ed6d1a864c74e0f2d916b03383345476c0aa1a860b3bae9ba6fbf370f6e309905d7ef5cb1186fd561b7d
7
+ data.tar.gz: b2678a54e99637e8d1706a6ccd913346d9e64d7ea0ef46e2c7e5637683844ab21220b6c1422601fc8466178358377e6da092cc0320cc5f6e427052b5e1b786f1
@@ -0,0 +1,133 @@
1
+ # Copilot Instructions for zen-service
2
+
3
+ ## Project Overview
4
+
5
+ `zen-service` is a Ruby gem providing a flexible, plugin-based service object pattern. The architecture emphasizes extensibility through a plugin system where even core functionality (callable, attributes) is implemented as plugins.
6
+
7
+ ## Architecture Fundamentals
8
+
9
+ ### Plugin System
10
+
11
+ The entire gem is built around `Zen::Service::Plugins` - a dynamic plugin registration and loading system:
12
+
13
+ - Plugins auto-register using `extend Zen::Service::Plugins::Plugin` (converts module name to snake_case key)
14
+ - Services use plugins via `use :plugin_name, **options`
15
+ - Plugin lifecycle: `used(service_class)` → includes module → `configure(service_class)` if defined
16
+ - See [plugin.rb](lib/zen/service/plugins/plugin.rb) for the DSL: `register_as`, `default_options`, `service_extension`
17
+
18
+ ### Core Plugins Architecture
19
+
20
+ Base `Zen::Service` uses two foundational plugins:
21
+
22
+ - `:callable` - provides `.call` and `.[]` class methods that instantiate + call
23
+ - `:attributes` - manages initialization parameters with runtime validation
24
+
25
+ ### Service Attributes Pattern
26
+
27
+ Attributes are positional-or-named parameters resolved during initialization:
28
+
29
+ ```ruby
30
+ attributes :foo, :bar
31
+ new(1, bar: 2) # foo=1, bar=2
32
+ new(foo: 1) # foo=1, bar=nil
33
+ ```
34
+
35
+ - Attributes generate reader methods dynamically in a dedicated `AttributeMethods` module
36
+ - Each service class gets its own `AttributeMethods` constant to isolate attribute methods
37
+ - `with_attributes(hash)` creates clones with merged attributes
38
+
39
+ ## Development Workflow
40
+
41
+ ### Running Tests
42
+
43
+ ```bash
44
+ bundle exec rspec spec # Run all specs
45
+ bundle exec rspec spec/zen/service_spec.rb # Single file
46
+ rake # Run specs + rubocop
47
+ ```
48
+
49
+ ### Test Patterns
50
+
51
+ - Use `def_service { ... }` helper to define service classes in specs (see [spec_helper.rb](spec/spec_helper.rb))
52
+ - `build_service(*args, **kwargs)` instantiates service with attributes
53
+ - Services are frozen_string_literal by convention
54
+
55
+ ### Building & Releasing
56
+
57
+ ```bash
58
+ rake build # Build gem to pkg/
59
+ rake install # Install locally
60
+ rake release # Tag + push to rubygems.org
61
+ ```
62
+
63
+ ## Key Conventions
64
+
65
+ ### Plugin Implementation Pattern
66
+
67
+ 1. Create module in `lib/zen/service/plugins/`
68
+ 2. `extend Zen::Service::Plugins::Plugin` (auto-registers)
69
+ 3. Define `used(service_class, **opts, &block)` for one-time setup
70
+ 4. Define `configure(service_class, **opts, &block)` for reconfiguration
71
+ 5. Use `prepend Extension` (for wrapping `call`) or `include` (for adding methods)
72
+ 6. Add `ClassMethods` module for class-level functionality
73
+
74
+ Example from [result_yielding.rb](lib/zen/service/plugins/result_yielding.rb):
75
+
76
+ ```ruby
77
+ module ResultYielding
78
+ extend Plugin
79
+
80
+ module Extension
81
+ def call
82
+ return super unless block_given?
83
+ result = nil
84
+ super { result = yield }
85
+ result
86
+ end
87
+ end
88
+
89
+ def self.used(service_class)
90
+ service_class.prepend(Extension)
91
+ end
92
+ end
93
+ ```
94
+
95
+ ### Plugin Option Handling
96
+
97
+ - Use `default_options foo: 5` in plugin definition
98
+ - Access via `self.class.plugins[:plugin_name].options[:foo]`
99
+ - Options merge with defaults when using plugin
100
+
101
+ ### Inheritance Behavior
102
+
103
+ - `attributes_list` is duplicated on inheritance
104
+ - Each subclass gets its own `AttributeMethods` module
105
+ - Plugin reflections accumulate through `ancestors.flat_map(&:service_plugins)`
106
+
107
+ ## Critical Implementation Details
108
+
109
+ ### Why Prepend vs Include
110
+
111
+ - `prepend Extension` - use when wrapping `call` method (allows `super` to reach original)
112
+ - `include` - use for adding new methods
113
+ - See `:result_yielding` (prepend) vs `:persisted_result` (extend in initialize)
114
+
115
+ ### Attributes Resolution Edge Cases
116
+
117
+ - Cannot pass same attribute as both positional and named
118
+ - Cannot pass more attributes than declared
119
+ - Args filled in declaration order, then kwargs merged
120
+
121
+ ### Plugin DSL Methods
122
+
123
+ From [plugin.rb](lib/zen/service/plugins/plugin.rb):
124
+
125
+ - `register_as :custom_name` - override auto-generated registration name
126
+ - `default_options hash` - set default plugin options
127
+ - `service_extension module` - extend `Zen::Service` base class globally
128
+
129
+ ## Files of Note
130
+
131
+ - [plugins.rb](lib/zen/service/plugins.rb) - central plugin registry with `fetch` and `register`
132
+ - [pluggable.rb](lib/zen/service/plugins/pluggable.rb) - `use` DSL and plugin reflection system
133
+ - [spec_helper.rb](spec/spec_helper.rb) - `def_service` pattern for testing services
data/README.md CHANGED
@@ -98,7 +98,9 @@ simplicity.
98
98
  However, `zen-service` still provides a couple of helpfull plugins out-of-the-box:
99
99
 
100
100
  - `:persisted_result` - provides `#result` method that returns value of the latest `#call`
101
- method call. Also provides `#called?` helper method.
101
+ method call. Also provides `#called?` helper method. Supports `call_unless_called` option.
102
+ When set to `true`, calling `service.result` method will call `#call` method if it has
103
+ not yet been called. Default value is `false`.
102
104
 
103
105
  - `:result_yielding` - can be used in junction with nested service calls to result with
104
106
  block-provided value instead of nested service `call` return value. For example:
@@ -160,6 +162,18 @@ end
160
162
  Todos::Show[todo] # => { id: 1, isCompleted: true }
161
163
  ```
162
164
 
165
+ **Note**: Custom plugins need to be registered before they can be used. Plugins that extend
166
+ `Zen::Service::Plugin` are automatically registered when the module is loaded. Alternatively,
167
+ you can register plugins manually:
168
+
169
+ ```rb
170
+ # Register a plugin module
171
+ Zen::Service::Plugins.register(:my_plugin, MyPlugin)
172
+
173
+ # Register by class name (useful when autoload isn't available yet, e.g., during Rails boot)
174
+ Zen::Service::Plugins.register(:my_plugin, "MyApp::Services::MyPlugin")
175
+ ```
176
+
163
177
  ## Development
164
178
 
165
179
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ostruct"
4
-
5
3
  module Zen
6
4
  module Service::Plugins
7
5
  module Callable
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ostruct"
4
-
5
3
  module Zen
6
4
  module Service::Plugins
7
5
  module PersistedResult
8
6
  extend Plugin
9
7
 
8
+ default_options call_unless_called: false
9
+
10
10
  module Extension
11
11
  def call
12
12
  @result = super
13
13
  end
14
14
  end
15
15
 
16
- attr_reader :result
17
-
18
16
  def initialize(*, **)
19
17
  super
20
18
  extend(Extension)
@@ -23,6 +21,12 @@ module Zen
23
21
  def called?
24
22
  defined?(@result)
25
23
  end
24
+
25
+ def result
26
+ call if self.class.plugins[:persisted_result].options[:call_unless_called] && !called?
27
+
28
+ @result
29
+ end
26
30
  end
27
31
  end
28
32
  end
@@ -11,7 +11,7 @@ module Zen
11
11
  defaults = extension.config[:default_options]
12
12
  opts = defaults.merge(opts) unless defaults.nil?
13
13
 
14
- if using?(name)
14
+ if service_plugins.key?(name)
15
15
  extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
16
16
  return extension
17
17
  end
@@ -23,8 +23,16 @@ module Zen
23
23
  plugins.key?(name)
24
24
  end
25
25
 
26
+ def service_plugins
27
+ @service_plugins ||= {}
28
+ end
29
+
26
30
  def plugins
27
- @plugins ||= {}
31
+ ancestors
32
+ .select { |klass| klass <= ::Zen::Service }
33
+ .flat_map(&:service_plugins)
34
+ .reverse
35
+ .reduce(&:merge)
28
36
  end
29
37
  alias extensions plugins
30
38
 
@@ -37,7 +45,7 @@ module Zen
37
45
  extension.used(self, **opts, &block) if extension.respond_to?(:used)
38
46
  extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
39
47
 
40
- plugins[name] = Reflection.new(extension, opts.merge(block:))
48
+ service_plugins[name] = Reflection.new(extension, opts.merge(block:))
41
49
 
42
50
  extension
43
51
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ostruct"
4
-
5
3
  module Zen
6
4
  module Service::Plugins
7
5
  module ResultYielding
@@ -3,25 +3,38 @@
3
3
  module Zen
4
4
  module Service::Plugins
5
5
  def self.fetch(name)
6
- require("zen/service/plugins/#{name}") unless plugins.key?(name)
6
+ raise("extension `#{name}` is not registered") unless plugins.key?(name)
7
7
 
8
- plugins[name] || raise("extension `#{name}` is not registered")
8
+ extension = plugins[name]
9
+ extension.is_a?(String) ? constantize(extension) : extension
9
10
  end
10
11
 
11
- def self.register(name, extension)
12
- raise(ArgumentError, "extension `#{name}` is already registered") if plugins.key?(name)
13
-
14
- plugins[name] =
15
- if (old_name = plugins.key(extension))
16
- plugins.delete(old_name)
17
- else
18
- extension
12
+ def self.register(name_or_hash, extension = nil)
13
+ if name_or_hash.is_a?(Hash)
14
+ name_or_hash.each do |name, ext|
15
+ register(name, ext)
19
16
  end
17
+ else
18
+ raise ArgumentError, "extension must be given" if extension.nil?
19
+
20
+ plugins[name_or_hash] =
21
+ if (old_name = plugins.key(extension))
22
+ plugins.delete(old_name)
23
+ else
24
+ extension
25
+ end
26
+ end
20
27
  end
21
28
 
22
29
  def self.plugins
23
30
  @plugins ||= {}
24
31
  end
32
+
33
+ def self.constantize(string)
34
+ return string.constantize if string.respond_to?(:constantize)
35
+
36
+ string.sub(/^::/, "").split("::").inject(Object) { |obj, const| obj.const_get(const) }
37
+ end
25
38
  end
26
39
 
27
40
  require_relative "plugins/plugin"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Zen
4
4
  class Service
5
- VERSION = "2.0.0"
5
+ VERSION = "2.2.0"
6
6
  end
7
7
  end
data/lib/zen/service.rb CHANGED
@@ -5,8 +5,6 @@ require_relative "service/plugins"
5
5
 
6
6
  module Zen
7
7
  class Service
8
- autoload :SpecHelpers, "zen/service/spec_helpers"
9
-
10
8
  extend Plugins::Pluggable
11
9
 
12
10
  use :callable
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zen-service
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Kuzko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-07 00:00:00.000000000 Z
11
+ date: 2025-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -101,6 +101,7 @@ executables: []
101
101
  extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
+ - ".github/copilot-instructions.md"
104
105
  - ".gitignore"
105
106
  - ".rspec"
106
107
  - ".rubocop.yml"