mozaik 1.0.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 +7 -0
- data/.github/workflows/ci.yml +18 -0
- data/.gitignore +43 -0
- data/.rspec +3 -0
- data/.rubocop.yaml +98 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +125 -0
- data/LICENSE.txt +21 -0
- data/README.md +314 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/mozaik/service/attribute_value.rb +46 -0
- data/lib/mozaik/service/errors.rb +100 -0
- data/lib/mozaik/service/failure.rb +20 -0
- data/lib/mozaik/service/result.rb +25 -0
- data/lib/mozaik/service/types.rb +22 -0
- data/lib/mozaik/service/version.rb +9 -0
- data/lib/mozaik/service.rb +133 -0
- data/mozaik.gemspec +30 -0
- metadata +145 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9795fb07d8cc2cd835fef4ff248de49f0d2d589ffbbb9882c915913aada3543a
|
|
4
|
+
data.tar.gz: 280f6e3a28ca194b8b85bd92362b8600fb78fcbdcc34a4afb0bcb25e4acf0d9d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '0969c0cc189968e3d00f322438495f405b89565a0aa9f487e4ea192cea888bad65d59d1ceec74e756da22fba1d22235b8531fdb3a2521c2bc1f746ab8631b2d5'
|
|
7
|
+
data.tar.gz: 1ca4478c06dd430a0145ca4ad3d029b8e65958997124e68950135823abe241b18cb2fc8569f18436969e977c1b4b6edf3476a66572d107ac7520308d5a58c11e
|
data/.gitignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Used by dotenv library to load environment variables.
|
|
14
|
+
# .env
|
|
15
|
+
|
|
16
|
+
# Ignore Byebug command history file.
|
|
17
|
+
.byebug_history
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Documentation cache and generated files:
|
|
21
|
+
/.yardoc/
|
|
22
|
+
/_yardoc/
|
|
23
|
+
/doc/
|
|
24
|
+
/rdoc/
|
|
25
|
+
|
|
26
|
+
## Environment normalization:
|
|
27
|
+
/.bundle/
|
|
28
|
+
/vendor/bundle
|
|
29
|
+
/lib/bundler/man/
|
|
30
|
+
|
|
31
|
+
# for a library or gem, you might want to ignore these files since the code is
|
|
32
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
33
|
+
# Gemfile.lock
|
|
34
|
+
# .ruby-version
|
|
35
|
+
# .ruby-gemset
|
|
36
|
+
|
|
37
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
38
|
+
.rvmrc
|
|
39
|
+
|
|
40
|
+
.rspec_status
|
|
41
|
+
|
|
42
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
|
43
|
+
# .rubocop-https?--*
|
data/.rspec
ADDED
data/.rubocop.yaml
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require: rubocop-rspec
|
|
2
|
+
AllCops:
|
|
3
|
+
TargetRubyVersion: 3.1.2
|
|
4
|
+
DisplayCopNames: true
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'bin/*'
|
|
7
|
+
- 'db/schema.rb'
|
|
8
|
+
- 'vendor/**/*'
|
|
9
|
+
- 'node_modules/**/*'
|
|
10
|
+
- '**/*.rake'
|
|
11
|
+
- 'licenses/*'
|
|
12
|
+
|
|
13
|
+
Documentation:
|
|
14
|
+
Enabled: false
|
|
15
|
+
|
|
16
|
+
Gemspec/RequiredRubyVersion:
|
|
17
|
+
Enabled: false
|
|
18
|
+
|
|
19
|
+
Metrics/LineLength:
|
|
20
|
+
Enabled: true
|
|
21
|
+
Max: 120
|
|
22
|
+
|
|
23
|
+
Metrics/BlockLength:
|
|
24
|
+
Enabled: true
|
|
25
|
+
Exclude:
|
|
26
|
+
- 'spec/**/*.rb'
|
|
27
|
+
- config/routes.rb
|
|
28
|
+
|
|
29
|
+
Style/Documentation:
|
|
30
|
+
Enabled: false
|
|
31
|
+
|
|
32
|
+
# This rule enforces the same delimiters to be used everywhere for %-literals.
|
|
33
|
+
# Each type of %-literal can be configured to use it's own specific notation when needed.
|
|
34
|
+
Style/PercentLiteralDelimiters:
|
|
35
|
+
PreferredDelimiters:
|
|
36
|
+
'%': ()
|
|
37
|
+
'%i': ()
|
|
38
|
+
'%q': ()
|
|
39
|
+
'%Q': ()
|
|
40
|
+
'%r': '{}'
|
|
41
|
+
'%s': ()
|
|
42
|
+
'%w': ()
|
|
43
|
+
'%W': ()
|
|
44
|
+
'%x': ()
|
|
45
|
+
|
|
46
|
+
Style/FrozenStringLiteralComment:
|
|
47
|
+
Enabled: true
|
|
48
|
+
|
|
49
|
+
Layout/EmptyLineAfterMagicComment:
|
|
50
|
+
Enabled: true
|
|
51
|
+
|
|
52
|
+
# This rule ensures that all Regex expression are written using the same style.
|
|
53
|
+
# The current configuration will be inline with the string array where %w is forced.
|
|
54
|
+
Style/RegexpLiteral:
|
|
55
|
+
EnforcedStyle: percent_r
|
|
56
|
+
AllowInnerSlashes: false
|
|
57
|
+
|
|
58
|
+
RSpec/MessageSpies:
|
|
59
|
+
EnforcedStyle: receive
|
|
60
|
+
|
|
61
|
+
RSpec/NestedGroups:
|
|
62
|
+
Enabled: false
|
|
63
|
+
|
|
64
|
+
RSpec/ExampleLength:
|
|
65
|
+
Enabled: false
|
|
66
|
+
|
|
67
|
+
Style/SymbolArray:
|
|
68
|
+
Enabled: true
|
|
69
|
+
|
|
70
|
+
Layout/EmptyLineAfterGuardClause:
|
|
71
|
+
Enabled: true
|
|
72
|
+
|
|
73
|
+
Naming/PredicateName:
|
|
74
|
+
Enabled: true
|
|
75
|
+
|
|
76
|
+
Style/EmptyLiteral:
|
|
77
|
+
Enabled: true
|
|
78
|
+
|
|
79
|
+
Style/ExpandPathArguments:
|
|
80
|
+
Enabled: true
|
|
81
|
+
|
|
82
|
+
Style/NumericLiterals:
|
|
83
|
+
Enabled: true
|
|
84
|
+
|
|
85
|
+
Style/WordArray:
|
|
86
|
+
Enabled: true
|
|
87
|
+
|
|
88
|
+
Layout/ClosingHeredocIndentation:
|
|
89
|
+
Enabled: true
|
|
90
|
+
|
|
91
|
+
RSpec/EmptyLineAfterExampleGroup:
|
|
92
|
+
Enabled: true
|
|
93
|
+
|
|
94
|
+
Style/RandomWithOffset:
|
|
95
|
+
Enabled: true
|
|
96
|
+
|
|
97
|
+
Style/WhileUntilModifier:
|
|
98
|
+
Enabled: true
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.3.8
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
mozaik (1.0.0)
|
|
5
|
+
activesupport
|
|
6
|
+
dry-struct (>= 1.0, < 2.0)
|
|
7
|
+
dry-types (>= 1.0, < 2.0)
|
|
8
|
+
i18n
|
|
9
|
+
|
|
10
|
+
GEM
|
|
11
|
+
remote: https://rubygems.org/
|
|
12
|
+
specs:
|
|
13
|
+
activesupport (8.0.2)
|
|
14
|
+
base64
|
|
15
|
+
benchmark (>= 0.3)
|
|
16
|
+
bigdecimal
|
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
18
|
+
connection_pool (>= 2.2.5)
|
|
19
|
+
drb
|
|
20
|
+
i18n (>= 1.6, < 2)
|
|
21
|
+
logger (>= 1.4.2)
|
|
22
|
+
minitest (>= 5.1)
|
|
23
|
+
securerandom (>= 0.3)
|
|
24
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
25
|
+
uri (>= 0.13.1)
|
|
26
|
+
ast (2.4.2)
|
|
27
|
+
base64 (0.3.0)
|
|
28
|
+
benchmark (0.4.1)
|
|
29
|
+
bigdecimal (3.2.2)
|
|
30
|
+
coderay (1.1.3)
|
|
31
|
+
concurrent-ruby (1.3.5)
|
|
32
|
+
connection_pool (2.5.3)
|
|
33
|
+
diff-lcs (1.5.0)
|
|
34
|
+
drb (2.2.3)
|
|
35
|
+
dry-core (1.1.0)
|
|
36
|
+
concurrent-ruby (~> 1.0)
|
|
37
|
+
logger
|
|
38
|
+
zeitwerk (~> 2.6)
|
|
39
|
+
dry-inflector (1.2.0)
|
|
40
|
+
dry-logic (1.6.0)
|
|
41
|
+
bigdecimal
|
|
42
|
+
concurrent-ruby (~> 1.0)
|
|
43
|
+
dry-core (~> 1.1)
|
|
44
|
+
zeitwerk (~> 2.6)
|
|
45
|
+
dry-struct (1.8.0)
|
|
46
|
+
dry-core (~> 1.1)
|
|
47
|
+
dry-types (~> 1.8, >= 1.8.2)
|
|
48
|
+
ice_nine (~> 0.11)
|
|
49
|
+
zeitwerk (~> 2.6)
|
|
50
|
+
dry-types (1.8.3)
|
|
51
|
+
bigdecimal (~> 3.0)
|
|
52
|
+
concurrent-ruby (~> 1.0)
|
|
53
|
+
dry-core (~> 1.0)
|
|
54
|
+
dry-inflector (~> 1.0)
|
|
55
|
+
dry-logic (~> 1.4)
|
|
56
|
+
zeitwerk (~> 2.6)
|
|
57
|
+
i18n (1.14.7)
|
|
58
|
+
concurrent-ruby (~> 1.0)
|
|
59
|
+
ice_nine (0.11.2)
|
|
60
|
+
json (2.6.3)
|
|
61
|
+
logger (1.7.0)
|
|
62
|
+
method_source (1.0.0)
|
|
63
|
+
minitest (5.25.5)
|
|
64
|
+
parallel (1.22.1)
|
|
65
|
+
parser (3.2.1.0)
|
|
66
|
+
ast (~> 2.4.1)
|
|
67
|
+
pry (0.14.2)
|
|
68
|
+
coderay (~> 1.1)
|
|
69
|
+
method_source (~> 1.0)
|
|
70
|
+
rainbow (3.1.1)
|
|
71
|
+
regexp_parser (2.7.0)
|
|
72
|
+
rexml (3.3.6)
|
|
73
|
+
strscan
|
|
74
|
+
rspec (3.12.0)
|
|
75
|
+
rspec-core (~> 3.12.0)
|
|
76
|
+
rspec-expectations (~> 3.12.0)
|
|
77
|
+
rspec-mocks (~> 3.12.0)
|
|
78
|
+
rspec-core (3.12.1)
|
|
79
|
+
rspec-support (~> 3.12.0)
|
|
80
|
+
rspec-expectations (3.12.2)
|
|
81
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
82
|
+
rspec-support (~> 3.12.0)
|
|
83
|
+
rspec-mocks (3.12.3)
|
|
84
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
85
|
+
rspec-support (~> 3.12.0)
|
|
86
|
+
rspec-support (3.12.0)
|
|
87
|
+
rubocop (1.45.1)
|
|
88
|
+
json (~> 2.3)
|
|
89
|
+
parallel (~> 1.10)
|
|
90
|
+
parser (>= 3.2.0.0)
|
|
91
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
92
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
93
|
+
rexml (>= 3.2.5, < 4.0)
|
|
94
|
+
rubocop-ast (>= 1.24.1, < 2.0)
|
|
95
|
+
ruby-progressbar (~> 1.7)
|
|
96
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
|
97
|
+
rubocop-ast (1.26.0)
|
|
98
|
+
parser (>= 3.2.1.0)
|
|
99
|
+
rubocop-capybara (2.17.0)
|
|
100
|
+
rubocop (~> 1.41)
|
|
101
|
+
rubocop-rspec (2.18.1)
|
|
102
|
+
rubocop (~> 1.33)
|
|
103
|
+
rubocop-capybara (~> 2.17)
|
|
104
|
+
ruby-progressbar (1.11.0)
|
|
105
|
+
securerandom (0.4.1)
|
|
106
|
+
strscan (3.1.0)
|
|
107
|
+
tzinfo (2.0.6)
|
|
108
|
+
concurrent-ruby (~> 1.0)
|
|
109
|
+
unicode-display_width (2.4.2)
|
|
110
|
+
uri (1.0.3)
|
|
111
|
+
zeitwerk (2.7.3)
|
|
112
|
+
|
|
113
|
+
PLATFORMS
|
|
114
|
+
arm64-darwin-24
|
|
115
|
+
|
|
116
|
+
DEPENDENCIES
|
|
117
|
+
bundler (~> 2.0)
|
|
118
|
+
mozaik!
|
|
119
|
+
pry
|
|
120
|
+
rspec
|
|
121
|
+
rubocop
|
|
122
|
+
rubocop-rspec
|
|
123
|
+
|
|
124
|
+
BUNDLED WITH
|
|
125
|
+
2.4.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Hive Technologies
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Mozaik::Service
|
|
2
|
+
|
|
3
|
+
This gem aims to simplify and standardize the service interface to be used across the service layer in our ruby projects. It provides composition, typing, and built in validations to ensure that our complex service logic is both flexible and safe.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'mozaik'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install mozaik
|
|
20
|
+
|
|
21
|
+
To install the gem locally you need to have your bundle configured to fetch from our org's private repos. Follow the following steps to do so:
|
|
22
|
+
1. Go to your github settings -> Developer settings -> Personal access tokens
|
|
23
|
+
2. Create a new token and check the repo permissions
|
|
24
|
+
3. Save your token somewhere
|
|
25
|
+
4. On your local machine use the following command:
|
|
26
|
+
```bash
|
|
27
|
+
$ bundle config github.com 'YOUR_TOKEN_HERE'
|
|
28
|
+
```
|
|
29
|
+
5. Restart your terminal session and bundling should work as expected!
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
We need to create a base service that will inherit from `::Mozaik::Service`:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
class BaseService < ::Mozaik::Service
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## .run
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class ExampleService < BaseService
|
|
47
|
+
attribute :counter, type: Types::Integer, required: true
|
|
48
|
+
|
|
49
|
+
validate :counter_not_exceeded
|
|
50
|
+
|
|
51
|
+
def perform
|
|
52
|
+
@counter + 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def counter_not_exceeded
|
|
58
|
+
add_error(:counter, :exceeded) if @counter > 10
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
```ruby
|
|
65
|
+
ExampleService.run(counter: 9)
|
|
66
|
+
|
|
67
|
+
=> Mozaik::Service::Result.new(result: 10, errors: nil, success: true)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The following happens:
|
|
71
|
+
- It will validate that the counter can be coerced in to integer
|
|
72
|
+
- If not, execution will be halted and service will fail
|
|
73
|
+
- It will run `counter_not_exceeded` method (if validation passes)
|
|
74
|
+
- If the validation fails (counter > 10), `perform` method will not be executed and the service will **fail**
|
|
75
|
+
- If the validation passes `perform` method will be executed
|
|
76
|
+
- `Result` object will be returned with `success=true` and `result=counter+1`
|
|
77
|
+
|
|
78
|
+
Note that:
|
|
79
|
+
```ruby
|
|
80
|
+
ExampleService.run('counter' => 9)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
is also a valid service invocation
|
|
84
|
+
|
|
85
|
+
## .run!
|
|
86
|
+
|
|
87
|
+
The `run!` method will raise an error `Mozaik::Service::Failure` if validation fails, otherwise it will return the returned value from the `perform` method without wrapping it in a `Result` object.
|
|
88
|
+
|
|
89
|
+
`Mozaik::Service::Failure` will have access to errors:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
begin
|
|
93
|
+
ExampleService.run!('counter' => 11)
|
|
94
|
+
rescue Mozaik::Service::Failure => error
|
|
95
|
+
error.errors.to_h
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
=> { counter: [type: :exceeded] }
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## #compose
|
|
103
|
+
|
|
104
|
+
The `compose` method allows us to compose multiple services within a service.
|
|
105
|
+
|
|
106
|
+
Usage:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
def perform
|
|
110
|
+
counter = @counter + 10
|
|
111
|
+
new_counter = compose AnotherService, counter: counter, other_param: true
|
|
112
|
+
new_counter + counter
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
It will:
|
|
117
|
+
|
|
118
|
+
- Call `AnotherService.run()` with given attributes
|
|
119
|
+
- If composed service fails **it will halt whole execution** and merge the errors with calling service's errors.
|
|
120
|
+
- If composed service succeeds it will return the unwrapped return value of the composed service's `perform` method
|
|
121
|
+
|
|
122
|
+
## Result object
|
|
123
|
+
|
|
124
|
+
Running the `run` method causes the service to return a `Result` object.
|
|
125
|
+
|
|
126
|
+
If the service succeeds:
|
|
127
|
+
- `Result` object will have `success=true`
|
|
128
|
+
- `Result` object will have returned value from `perform` method inside `result` attribute
|
|
129
|
+
- `Result` object will have empty `errors`
|
|
130
|
+
|
|
131
|
+
If the service fails:
|
|
132
|
+
- `Result` object will have `success=false`
|
|
133
|
+
- `Result` object will have `result=nil`
|
|
134
|
+
- `Result` object will have one or more errors in the `errors` attribute
|
|
135
|
+
|
|
136
|
+
## Errors
|
|
137
|
+
|
|
138
|
+
Each instance of this class will have `@errors`. At any point for lifecycle you can add errors.
|
|
139
|
+
After each step (validations, execution) service will check if there are some errors - if yes it will halt execution and return `Result` object
|
|
140
|
+
|
|
141
|
+
to add error use `add_error` method:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
add_error(:some_field, :this_is_an_error)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Note: this will add error but will not halt current step
|
|
148
|
+
|
|
149
|
+
To add error and halt execution immediately use:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
add_error!(:some_field, :this_is_error)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Errors object
|
|
156
|
+
|
|
157
|
+
Errors object is similar to hash but with some adjustments.
|
|
158
|
+
If you only care about error values then you can run `#to_h`.
|
|
159
|
+
If you want also full messages or the translations then run `#full_details`.
|
|
160
|
+
|
|
161
|
+
It will return a hash similar to:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
{
|
|
165
|
+
counter: {
|
|
166
|
+
type: :exceeded,
|
|
167
|
+
message: 'Counter was exceeded'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
This gem uses `I18n` for translation, it will look for a translation key as follows:
|
|
173
|
+
`[service_name].errors.[some_attribute].[error_name]`.
|
|
174
|
+
|
|
175
|
+
The `[service_name]` is the class of service name converted to under score like:
|
|
176
|
+
|
|
177
|
+
`SomeNamespace::SomeModule::ExampleService` -> `some_namespace.some_module.some_service`
|
|
178
|
+
|
|
179
|
+
For errors coming from composed services, it will try to find translation for **outer** (caller) service first
|
|
180
|
+
and then for **inner** (composed) service.
|
|
181
|
+
|
|
182
|
+
## Attributes
|
|
183
|
+
|
|
184
|
+
Attributes are validated/coerced using `Dry::Types`.
|
|
185
|
+
|
|
186
|
+
To define a type for an attribute use:
|
|
187
|
+
```ruby
|
|
188
|
+
attribute :some_attribute, type: Types::[some-type]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If the passed value has a different type the service will fail and add an error:
|
|
192
|
+
```ruby
|
|
193
|
+
errors.add(:some_attribute, :wrong_type)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The value will be accessible by an **instance variable**: `@some_attribute`
|
|
197
|
+
|
|
198
|
+
So for example:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
attribute :cool_attribute, type: Types::Integer
|
|
202
|
+
|
|
203
|
+
def perform
|
|
204
|
+
@cool_attribute + 1
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
It will be accessible by an instance method you define inside your service class.
|
|
209
|
+
|
|
210
|
+
By default `Types` module has imported coercible types, if you want to use strict ones (that will raise error if passed attribute does not have exactly same type) you need to call it explicitly by using `Strict`:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
attribute :some_attribute, type: Types::Strict::[some-type]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
By default every attribute is **not required**. You can change that by passing `required: true` option to the attribute
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
attribute :some_attribute, type: Types::String, required: true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
In this case, if `some_attribute` will be missing service will add an error:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
errors.add(:some_attribute, :blank)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
You can also specify a default value for given attribute:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
attribute :some_attribute, type: Types::String, default: 'this is the default value'
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If `some_attribute` will not be passed to service it will use the defined default value. _Default values will be coerced_.
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
### Basic types
|
|
238
|
+
|
|
239
|
+
|type|example coercion|
|
|
240
|
+
|---|---|
|
|
241
|
+
|Types::String| `123 -> '123'`, `:symbol -> 'symbol'`|
|
|
242
|
+
|Types::Symbol| `'symbol' -> :symbol`|
|
|
243
|
+
|Types::Integer| `'123' -> 123`, `123.13 -> 123`|
|
|
244
|
+
|Types::Float| `'123.123' -> 123.123`, `123 -> 123.0`|
|
|
245
|
+
|Types::Date| `'2010-10-10' -> Date.parse('2010-10-10')`|
|
|
246
|
+
|Types::Time| `'2010-10-10 10:10' -> Time.parse('2010-10-10 10:10')`|
|
|
247
|
+
|Types::Bool| `'false' -> false`, `'1' -> true`|
|
|
248
|
+
|
|
249
|
+
### Instance type
|
|
250
|
+
|
|
251
|
+
Instance can be used in two ways:
|
|
252
|
+
1) Pure `dry-types`
|
|
253
|
+
2) Pure class
|
|
254
|
+
|
|
255
|
+
|type|example|
|
|
256
|
+
|---|---|
|
|
257
|
+
|Types::Instance(SomeType) | `SomeType.new` |
|
|
258
|
+
|SomeType | `SomeType.new` |
|
|
259
|
+
|
|
260
|
+
The gem will wrap anything passed as `type` of attribute which is not a child of `Dry::Types::Type` (or `Array`) into `Types::Instance()`
|
|
261
|
+
|
|
262
|
+
### Array type
|
|
263
|
+
|
|
264
|
+
Array can be used in two ways:
|
|
265
|
+
1) Pure `dry-types`
|
|
266
|
+
2) Wrapped in syntax sugar
|
|
267
|
+
|
|
268
|
+
|type|example|
|
|
269
|
+
|---|---|
|
|
270
|
+
|`Types::Array(Types::Symbol)` | `[:symbol, 'other-symbol']` |
|
|
271
|
+
|`[Types::Symbol]` | `[:symbol, 'other-symbol']` |
|
|
272
|
+
|`[SomeType]` | `[SomeType.new]` |
|
|
273
|
+
|`Types::Array(Types::Instance(SomeType))` | `[SomeType.new]` |
|
|
274
|
+
|
|
275
|
+
Using syntax sugar will allow us to pass normal classes inside `[]` and this classes will be wrapped into
|
|
276
|
+
`Types::Instance()`
|
|
277
|
+
|
|
278
|
+
### Interface type
|
|
279
|
+
|
|
280
|
+
Interface type should be used with `dry-type` `Interface` syntax:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
attribute :some_attribute, type: Types::Interface(:some_method)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Hash type
|
|
287
|
+
|
|
288
|
+
Interface type should be used with `dry-type` `Hash` sytnax:
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
attribute :some_attribute, type: Types::Hash(some_key: Types::Integer)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Note if you want to use classes for nested types then you need to wrap it into `Types::Instance()`
|
|
295
|
+
|
|
296
|
+
### Any type
|
|
297
|
+
|
|
298
|
+
Any type should be used with `dry-type` `Any` syntax:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
attribute :some_attribute, type: Types::Any
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
> The use of `Any` type should be limited as much as possible since it contradicts the whole concept of typing in the first place!
|
|
305
|
+
|
|
306
|
+
### Any any type
|
|
307
|
+
|
|
308
|
+
You can use any type that comes from `dry-types`. For reference please see: [dry-types](https://dry-rb.org/gems/dry-types/1.2/built-in-types/)
|
|
309
|
+
|
|
310
|
+
### Examples
|
|
311
|
+
|
|
312
|
+
You can find implementation examples in `spec/mozaik/service_spec.rb`
|
|
313
|
+
|
|
314
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'mozaik/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(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mozaik
|
|
4
|
+
class Service
|
|
5
|
+
class AttributeValue
|
|
6
|
+
AttributeBlank = Class.new(StandardError)
|
|
7
|
+
AttributeWithWrongType = Class.new(StandardError)
|
|
8
|
+
|
|
9
|
+
def initialize(value, options)
|
|
10
|
+
@value = value
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
value = @value
|
|
16
|
+
value = @options[:default] if value.nil? && @options.key?(:default)
|
|
17
|
+
type[value]
|
|
18
|
+
rescue Dry::Types::CoercionError, Dry::Types::MissingKeyError, Dry::Types::SchemaError
|
|
19
|
+
raise AttributeBlank if @value.nil? && @options[:required]
|
|
20
|
+
|
|
21
|
+
raise AttributeWithWrongType
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def type
|
|
25
|
+
return @type if @type
|
|
26
|
+
|
|
27
|
+
@type = determine_type(@options[:type])
|
|
28
|
+
@type = @type.optional unless @options[:required]
|
|
29
|
+
@type
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def determine_type(type)
|
|
33
|
+
case type
|
|
34
|
+
when Dry::Types::Type
|
|
35
|
+
type
|
|
36
|
+
when Array
|
|
37
|
+
Types::Array(determine_type(type.first))
|
|
38
|
+
else
|
|
39
|
+
Types.Instance(type)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mozaik
|
|
4
|
+
class Service
|
|
5
|
+
class Errors
|
|
6
|
+
def initialize(service)
|
|
7
|
+
@service_class = service.class
|
|
8
|
+
@errors = {}
|
|
9
|
+
@error_sources = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add(name, type)
|
|
13
|
+
return self if error?(name, type)
|
|
14
|
+
|
|
15
|
+
name = name.to_sym
|
|
16
|
+
@error_sources[name] ||= {}
|
|
17
|
+
@error_sources[name][type] = @service_class
|
|
18
|
+
|
|
19
|
+
@errors[name] ||= []
|
|
20
|
+
@errors[name] << { type: type.to_sym }
|
|
21
|
+
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def [](key)
|
|
26
|
+
@errors[key]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
to_h == other.to_h
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
@errors
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def empty?
|
|
38
|
+
@errors.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def error?(name, type)
|
|
42
|
+
(self[name.to_sym] || []).any? { |error| error.fetch(:type) == type.to_sym }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def inspect
|
|
46
|
+
"#{self.class} #{@errors}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def merge(other)
|
|
50
|
+
merge_errors(other.errors)
|
|
51
|
+
merge_error_sources(other.error_sources)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def full_details
|
|
56
|
+
@errors.map do |key, errors|
|
|
57
|
+
[key, errors.map { |error| { type: error[:type], message: translate_error(key, error[:type]) } }]
|
|
58
|
+
end.to_h
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def as_json(_options)
|
|
62
|
+
to_h
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
protected
|
|
66
|
+
|
|
67
|
+
attr_reader :errors, :error_sources
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def merge_errors(other_errors)
|
|
72
|
+
other_errors.each do |name, errors|
|
|
73
|
+
@errors[name] ||= []
|
|
74
|
+
@errors[name] = (@errors[name] + errors).uniq
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def merge_error_sources(other_error_sources)
|
|
79
|
+
@error_sources = @error_sources.deep_merge(other_error_sources)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def translate_error(key, type)
|
|
83
|
+
original_service_class = @error_sources[key][type]
|
|
84
|
+
[@service_class, original_service_class]
|
|
85
|
+
.uniq
|
|
86
|
+
.map { |service_class| translate(key, type, service_class) }
|
|
87
|
+
.select(&:itself)
|
|
88
|
+
.first
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def translate(name, type, service_class)
|
|
92
|
+
prefix = service_class.to_s.underscore.tr('/', '.')
|
|
93
|
+
key = [prefix, 'errors', name, type].join('.')
|
|
94
|
+
I18n.exists?(key) && I18n.t(key)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mozaik
|
|
4
|
+
class Service
|
|
5
|
+
class Failure < StandardError
|
|
6
|
+
attr_reader :errors
|
|
7
|
+
|
|
8
|
+
def initialize(errors)
|
|
9
|
+
super
|
|
10
|
+
@errors = errors
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def message
|
|
14
|
+
"failed with errors #{errors.to_h}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mozaik
|
|
4
|
+
class Service
|
|
5
|
+
class Result < Dry::Struct
|
|
6
|
+
attribute :success, Types::Bool
|
|
7
|
+
attribute :result, Types::Any
|
|
8
|
+
attribute :errors, Types.Instance(Errors) | Types::Nil
|
|
9
|
+
|
|
10
|
+
def success?
|
|
11
|
+
success
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def failure?
|
|
15
|
+
!success
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def successful?
|
|
19
|
+
success
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mozaik
|
|
4
|
+
class Service
|
|
5
|
+
module Types
|
|
6
|
+
# Expose Dry types with coercible defaults matching original behavior
|
|
7
|
+
include Dry.Types(default: :params)
|
|
8
|
+
|
|
9
|
+
Any = Nominal::Any
|
|
10
|
+
String = Coercible::String
|
|
11
|
+
Symbol = Coercible::Symbol
|
|
12
|
+
Integer = Coercible::Integer
|
|
13
|
+
Float = Coercible::Float
|
|
14
|
+
Date = Params::Date
|
|
15
|
+
Time = Params::Time
|
|
16
|
+
Bool = Params::Bool
|
|
17
|
+
Class = Strict::Class
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-types'
|
|
4
|
+
require 'dry-struct'
|
|
5
|
+
require 'i18n'
|
|
6
|
+
require 'active_support/core_ext/hash/deep_merge'
|
|
7
|
+
require 'active_support/core_ext/hash/keys'
|
|
8
|
+
require 'active_support/core_ext/string/inflections'
|
|
9
|
+
|
|
10
|
+
require 'mozaik/service/version'
|
|
11
|
+
require 'mozaik/service/errors'
|
|
12
|
+
require 'mozaik/service/types'
|
|
13
|
+
require 'mozaik/service/result'
|
|
14
|
+
require 'mozaik/service/failure'
|
|
15
|
+
require 'mozaik/service/attribute_value'
|
|
16
|
+
|
|
17
|
+
module Mozaik
|
|
18
|
+
class Service
|
|
19
|
+
class Halt < StandardError; end
|
|
20
|
+
|
|
21
|
+
def self.attribute(name, type:, required: false, default: nil)
|
|
22
|
+
@attributes ||= {}
|
|
23
|
+
@attributes[name] = { type: type, required: required, default: default }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.attributes
|
|
27
|
+
@attributes || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.attributes_names
|
|
31
|
+
attributes.keys
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.validations
|
|
35
|
+
@validations ||= []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.validate(method, *args)
|
|
39
|
+
validations.push([method, args]) unless validations.include?([method, args])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.run(attributes = {})
|
|
43
|
+
new(attributes).run
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.run!(attributes = {})
|
|
47
|
+
outcome = run(attributes)
|
|
48
|
+
raise Failure, outcome.errors unless outcome.success?
|
|
49
|
+
|
|
50
|
+
outcome.result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(attributes = {})
|
|
54
|
+
initialize_errors
|
|
55
|
+
initialize_inputs(attributes)
|
|
56
|
+
initialize_attributes
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def run
|
|
60
|
+
halt! unless valid?
|
|
61
|
+
validate
|
|
62
|
+
halt! unless valid?
|
|
63
|
+
result = perform
|
|
64
|
+
halt! unless valid?
|
|
65
|
+
return_success(result)
|
|
66
|
+
rescue Halt
|
|
67
|
+
return_failure(@errors)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def compose(other_service_class, params = {})
|
|
73
|
+
outcome = other_service_class.run(params)
|
|
74
|
+
return outcome.result if outcome.success?
|
|
75
|
+
|
|
76
|
+
@errors.merge(outcome.errors)
|
|
77
|
+
halt!
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_error(name, error)
|
|
81
|
+
@errors.add(name, error)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def add_error!(name, error)
|
|
85
|
+
add_error(name, error)
|
|
86
|
+
halt!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate
|
|
90
|
+
self.class.validations.each { |method, _| send(method) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def valid?
|
|
94
|
+
@errors.empty?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def halt!
|
|
98
|
+
raise Halt
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def return_success(result)
|
|
102
|
+
Result.new(result: result, success: true, errors: nil)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def return_failure(errors)
|
|
106
|
+
Result.new(result: nil, success: false, errors: errors)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def initialize_errors
|
|
110
|
+
@errors = Errors.new(self)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def initialize_inputs(attributes)
|
|
114
|
+
@inputs = attributes.deep_symbolize_keys
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def initialize_attributes
|
|
118
|
+
self.class.attributes.each { |name, options| initialize_attribute(name, options) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def initialize_attribute(name, options)
|
|
122
|
+
value = AttributeValue.new(@inputs[name], options).call
|
|
123
|
+
|
|
124
|
+
instance_variable_set(:"@#{name}", value)
|
|
125
|
+
rescue AttributeValue::AttributeBlank
|
|
126
|
+
@errors.add(name, :blank)
|
|
127
|
+
rescue AttributeValue::AttributeWithWrongType
|
|
128
|
+
@errors.add(name, :wrong_type)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
|
data/mozaik.gemspec
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'mozaik/service/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'mozaik'
|
|
9
|
+
spec.version = Mozaik::Service::VERSION
|
|
10
|
+
spec.authors = ['Mohamed Elmenisy']
|
|
11
|
+
spec.email = ['mohamed.elmenisy@hive.app']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'Service layer abstraction'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
|
|
16
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
17
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
spec.bindir = 'exe'
|
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
22
|
+
spec.require_paths = ['lib']
|
|
23
|
+
|
|
24
|
+
spec.add_runtime_dependency 'activesupport'
|
|
25
|
+
spec.add_runtime_dependency 'dry-struct', '>= 1.0', '< 2.0'
|
|
26
|
+
spec.add_runtime_dependency 'dry-types', '>= 1.0', '< 2.0'
|
|
27
|
+
spec.add_runtime_dependency 'i18n'
|
|
28
|
+
|
|
29
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mozaik
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mohamed Elmenisy
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-08-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: dry-struct
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
- - "<"
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '2.0'
|
|
37
|
+
type: :runtime
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '1.0'
|
|
44
|
+
- - "<"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: dry-types
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
54
|
+
- - "<"
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '2.0'
|
|
57
|
+
type: :runtime
|
|
58
|
+
prerelease: false
|
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '1.0'
|
|
64
|
+
- - "<"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '2.0'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: i18n
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
type: :runtime
|
|
75
|
+
prerelease: false
|
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
- !ruby/object:Gem::Dependency
|
|
82
|
+
name: bundler
|
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '2.0'
|
|
88
|
+
type: :development
|
|
89
|
+
prerelease: false
|
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '2.0'
|
|
95
|
+
description:
|
|
96
|
+
email:
|
|
97
|
+
- mohamed.elmenisy@hive.app
|
|
98
|
+
executables: []
|
|
99
|
+
extensions: []
|
|
100
|
+
extra_rdoc_files: []
|
|
101
|
+
files:
|
|
102
|
+
- ".github/workflows/ci.yml"
|
|
103
|
+
- ".gitignore"
|
|
104
|
+
- ".rspec"
|
|
105
|
+
- ".rubocop.yaml"
|
|
106
|
+
- ".ruby-version"
|
|
107
|
+
- Gemfile
|
|
108
|
+
- Gemfile.lock
|
|
109
|
+
- LICENSE.txt
|
|
110
|
+
- README.md
|
|
111
|
+
- Rakefile
|
|
112
|
+
- bin/console
|
|
113
|
+
- bin/setup
|
|
114
|
+
- lib/mozaik/service.rb
|
|
115
|
+
- lib/mozaik/service/attribute_value.rb
|
|
116
|
+
- lib/mozaik/service/errors.rb
|
|
117
|
+
- lib/mozaik/service/failure.rb
|
|
118
|
+
- lib/mozaik/service/result.rb
|
|
119
|
+
- lib/mozaik/service/types.rb
|
|
120
|
+
- lib/mozaik/service/version.rb
|
|
121
|
+
- mozaik.gemspec
|
|
122
|
+
homepage:
|
|
123
|
+
licenses:
|
|
124
|
+
- MIT
|
|
125
|
+
metadata: {}
|
|
126
|
+
post_install_message:
|
|
127
|
+
rdoc_options: []
|
|
128
|
+
require_paths:
|
|
129
|
+
- lib
|
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '0'
|
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '0'
|
|
140
|
+
requirements: []
|
|
141
|
+
rubygems_version: 3.5.22
|
|
142
|
+
signing_key:
|
|
143
|
+
specification_version: 4
|
|
144
|
+
summary: Service layer abstraction
|
|
145
|
+
test_files: []
|