zen-service 2.1.0 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83f9ef4ebaca920508cb7aa96f3ce4eea6b899338b000c5a38edc6cacf342473
4
- data.tar.gz: 86a27632c1fc83f7ba7c735bea83c41a1b0d9e52b2d546bfdd3054e4766c447d
3
+ metadata.gz: 207425486ecb408db660e845768e30e9cd19aa3ed1d6a7214d41fc0eecd46590
4
+ data.tar.gz: 74787fb37ce4bc2e6d7c139e7ca7b245c25fcafd6711a1aba434c0e681007b9b
5
5
  SHA512:
6
- metadata.gz: 755c8e023bed8067af7fd509018ff8a001cb5618cde43fc00aa6b0244165482d58fadea79becc7b4ebb9f6a1e87aef9d9901aebd54e2218390553c2fcd71edbc
7
- data.tar.gz: 55ae83ddff091119dff6b1448e37e704dcdf39cdc616fcc11b51cccd3c979113e41df8396c1cb5688c459dcc3002534ea2f512c54815a3ceecf85cc74e89d8b5
6
+ metadata.gz: 4bc0fe9454e82c5b49f9ab87e3812e002691002d1476b9258417fa12ddae5419ea3621de952c821b27716078e9008745acf02e00946a5538570dd89c3a020972
7
+ data.tar.gz: e26b152b882e1fc74ef35a99644bcc3b976c8710eb8c5833b5393edf0843f190006a019c157ba2958a082d03cedcf4c989fd2b6d5788c0bb27f36d0e9f63391f
@@ -0,0 +1,141 @@
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 (first use): `used(service_class)` → includes module → `configure(service_class)` if defined
16
+ - Plugin reconfiguration (ancestor already used): only `configure(service_class)` is called, module not re-included
17
+ - See [plugin.rb](lib/zen/service/plugins/plugin.rb) for the DSL: `register_as`, `default_options`, `service_extension`
18
+
19
+ ### Core Plugins Architecture
20
+
21
+ Base `Zen::Service` uses two foundational plugins:
22
+
23
+ - `:callable` - provides `.call` and `.[]` class methods that instantiate + call
24
+ - `:attributes` - manages initialization parameters with runtime validation
25
+
26
+ ### Service Attributes Pattern
27
+
28
+ Attributes are positional-or-named parameters resolved during initialization:
29
+
30
+ ```ruby
31
+ attributes :foo, :bar
32
+ new(1, bar: 2) # foo=1, bar=2
33
+ new(foo: 1) # foo=1, bar=nil
34
+ ```
35
+
36
+ - Attributes generate reader methods dynamically in a dedicated `AttributeMethods` module
37
+ - Each service class gets its own `AttributeMethods` constant to isolate attribute methods
38
+ - `with_attributes(hash)` creates clones with merged attributes
39
+
40
+ ## Development Workflow
41
+
42
+ ### Running Tests
43
+
44
+ ```bash
45
+ bundle exec rspec spec # Run all specs
46
+ bundle exec rspec spec/zen/service_spec.rb # Single file
47
+ rake # Run specs + rubocop
48
+ ```
49
+
50
+ ### Test Patterns
51
+
52
+ - Use `def_service { ... }` helper to define service classes in specs (see [spec_helper.rb](spec/spec_helper.rb))
53
+ - `build_service(*args, **kwargs)` instantiates service with attributes
54
+ - Services are frozen_string_literal by convention
55
+
56
+ ### Building & Releasing
57
+
58
+ ```bash
59
+ rake build # Build gem to pkg/
60
+ rake install # Install locally
61
+ rake release # Tag + push to rubygems.org
62
+ ```
63
+
64
+ ## Key Conventions
65
+
66
+ ### Plugin Implementation Pattern
67
+
68
+ 1. Create module in `lib/zen/service/plugins/`
69
+ 2. `extend Zen::Service::Plugins::Plugin` (auto-registers)
70
+ 3. Define `used(service_class, **opts, &block)` for one-time setup
71
+ 4. Define `configure(service_class, **opts, &block)` for reconfiguration
72
+ 5. Use `prepend Extension` (for wrapping `call`) or `include` (for adding methods)
73
+ 6. Add `ClassMethods` module for class-level functionality
74
+
75
+ Example from [result_yielding.rb](lib/zen/service/plugins/result_yielding.rb):
76
+
77
+ ```ruby
78
+ module ResultYielding
79
+ extend Plugin
80
+
81
+ module Extension
82
+ def call
83
+ return super unless block_given?
84
+ result = nil
85
+ super { result = yield }
86
+ result
87
+ end
88
+ end
89
+
90
+ def self.used(service_class)
91
+ service_class.prepend(Extension)
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Plugin Option Handling
97
+
98
+ - Use `default_options foo: 5` in plugin definition
99
+ - Access via `self.class.plugins[:plugin_name].options[:foo]`
100
+ - Options merge with defaults when using plugin
101
+ - Blocks passed to `use` are stored in `reflection.block`, separate from options (not polluting options hash)
102
+
103
+ ### Plugin Inheritance & Reconfiguration
104
+
105
+ - When a child class uses a plugin already used by an ancestor, only `configure` callback is invoked (not `used`)
106
+ - This allows child classes to reconfigure plugin behavior without re-including the module
107
+ - Example: `BaseService` uses `:persisted_result` with default options, `ChildService` can reconfigure with different options
108
+
109
+ ### Inheritance Behavior
110
+
111
+ - `attributes_list` is duplicated on inheritance
112
+ - Each subclass gets its own `AttributeMethods` module
113
+ - Plugin reflections accumulate through `ancestors.flat_map(&:service_plugins)`
114
+
115
+ ## Critical Implementation Details
116
+
117
+ ### Why Prepend vs Include
118
+
119
+ - `prepend Extension` - use when wrapping `call` method (allows `super` to reach original)
120
+ - `include` - use for adding new methods
121
+ - See `:result_yielding` (prepend) vs `:persisted_result` (extend in initialize)
122
+
123
+ ### Attributes Resolution Edge Cases
124
+
125
+ - Cannot pass same attribute as both positional and named
126
+ - Cannot pass more attributes than declared
127
+ - Args filled in declaration order, then kwargs merged
128
+
129
+ ### Plugin DSL Methods
130
+
131
+ From [plugin.rb](lib/zen/service/plugins/plugin.rb):
132
+
133
+ - `register_as :custom_name` - override auto-generated registration name
134
+ - `default_options hash` - set default plugin options
135
+ - `service_extension module` - extend `Zen::Service` base class globally
136
+
137
+ ## Files of Note
138
+
139
+ - [plugins.rb](lib/zen/service/plugins.rb) - central plugin registry with `fetch` and `register`
140
+ - [pluggable.rb](lib/zen/service/plugins/pluggable.rb) - `use` DSL and plugin reflection system
141
+ - [spec_helper.rb](spec/spec_helper.rb) - `def_service` pattern for testing services
data/README.md CHANGED
@@ -116,6 +116,26 @@ However, `zen-service` still provides a couple of helpfull plugins out-of-the-bo
116
116
  end
117
117
  ```
118
118
 
119
+ #### Plugin Lifecycle
120
+
121
+ When using a plugin on a service class:
122
+
123
+ - If the plugin is used for the first time, both `used` and `configure` callbacks are invoked
124
+ - If the plugin was already used by an ancestor class, only the `configure` callback is invoked,
125
+ allowing reconfiguration without re-including the module
126
+
127
+ This design allows child classes to customize plugin behavior inherited from parent classes:
128
+
129
+ ```rb
130
+ class BaseService < Zen::Service
131
+ use :persisted_result, call_unless_called: false
132
+ end
133
+
134
+ class ChildService < BaseService
135
+ use :persisted_result, call_unless_called: true # Only reconfigures, doesn't re-include
136
+ end
137
+ ```
138
+
119
139
  Bellow you can see sample implementation of a plugin that transforms resulting objects
120
140
  to camel-case notation (relying on ActiveSupport's core extensions)
121
141
 
@@ -162,6 +182,18 @@ end
162
182
  Todos::Show[todo] # => { id: 1, isCompleted: true }
163
183
  ```
164
184
 
185
+ **Note**: Custom plugins need to be registered before they can be used. Plugins that extend
186
+ `Zen::Service::Plugin` are automatically registered when the module is loaded. Alternatively,
187
+ you can register plugins manually:
188
+
189
+ ```rb
190
+ # Register a plugin module
191
+ Zen::Service::Plugins.register(:my_plugin, MyPlugin)
192
+
193
+ # Register by class name (useful when autoload isn't available yet, e.g., during Rails boot)
194
+ Zen::Service::Plugins.register(:my_plugin, "MyApp::Services::MyPlugin")
195
+ ```
196
+
165
197
  ## Development
166
198
 
167
199
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
@@ -3,7 +3,7 @@
3
3
  module Zen
4
4
  module Service::Plugins
5
5
  module Pluggable
6
- Reflection = Struct.new(:extension, :options)
6
+ Reflection = Struct.new(:extension, :options, :block)
7
7
 
8
8
  def use(name, **opts, &block)
9
9
  extension = Service::Plugins.fetch(name)
@@ -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 service_plugins.key?(name)
14
+ if plugins.key?(name)
15
15
  extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
16
16
  return extension
17
17
  end
@@ -45,7 +45,7 @@ module Zen
45
45
  extension.used(self, **opts, &block) if extension.respond_to?(:used)
46
46
  extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
47
47
 
48
- service_plugins[name] = Reflection.new(extension, opts.merge(block:))
48
+ service_plugins[name] = Reflection.new(extension, opts, block)
49
49
 
50
50
  extension
51
51
  end
@@ -3,9 +3,10 @@
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
12
  def self.register(name_or_hash, extension = nil)
@@ -28,6 +29,12 @@ module Zen
28
29
  def self.plugins
29
30
  @plugins ||= {}
30
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
31
38
  end
32
39
 
33
40
  require_relative "plugins/plugin"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Zen
4
4
  class Service
5
- VERSION = "2.1.0"
5
+ VERSION = "2.2.1"
6
6
  end
7
7
  end
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.1.0
4
+ version: 2.2.1
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-20 00:00:00.000000000 Z
11
+ date: 2025-12-29 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"