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 +4 -4
- data/.github/copilot-instructions.md +141 -0
- data/README.md +32 -0
- data/lib/zen/service/plugins/pluggable.rb +3 -3
- data/lib/zen/service/plugins.rb +9 -2
- data/lib/zen/service/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 207425486ecb408db660e845768e30e9cd19aa3ed1d6a7214d41fc0eecd46590
|
|
4
|
+
data.tar.gz: 74787fb37ce4bc2e6d7c139e7ca7b245c25fcafd6711a1aba434c0e681007b9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
48
|
+
service_plugins[name] = Reflection.new(extension, opts, block)
|
|
49
49
|
|
|
50
50
|
extension
|
|
51
51
|
end
|
data/lib/zen/service/plugins.rb
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module Zen
|
|
4
4
|
module Service::Plugins
|
|
5
5
|
def self.fetch(name)
|
|
6
|
-
|
|
6
|
+
raise("extension `#{name}` is not registered") unless plugins.key?(name)
|
|
7
7
|
|
|
8
|
-
plugins[name]
|
|
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"
|
data/lib/zen/service/version.rb
CHANGED
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
|
|
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-
|
|
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"
|