featury 1.0.0.rc1 → 1.0.0.rc3
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/README.md +107 -3
- data/config/locales/en.yml +101 -0
- data/config/locales/ru.yml +101 -0
- data/lib/featury/actions/action.rb +14 -0
- data/lib/featury/actions/collection.rb +22 -0
- data/lib/featury/actions/dsl.rb +33 -0
- data/lib/featury/actions/service/builder.rb +52 -0
- data/lib/featury/actions/service/factory.rb +89 -0
- data/lib/featury/actions/workspace.rb +31 -0
- data/lib/featury/base.rb +12 -0
- data/lib/featury/conditions/collection.rb +14 -0
- data/lib/featury/conditions/condition.rb +13 -0
- data/lib/featury/conditions/dsl.rb +29 -0
- data/lib/featury/context/callable.rb +35 -0
- data/lib/featury/context/dsl.rb +12 -0
- data/lib/featury/context/workspace.rb +49 -0
- data/lib/featury/expert.rb +5 -0
- data/lib/featury/features/collection.rb +18 -0
- data/lib/featury/features/dsl.rb +35 -0
- data/lib/featury/features/feature.rb +14 -0
- data/lib/featury/groups/collection.rb +36 -0
- data/lib/featury/groups/dsl.rb +31 -0
- data/lib/featury/groups/group.rb +13 -0
- data/lib/featury/resources/collection.rb +22 -0
- data/lib/featury/resources/dsl.rb +29 -0
- data/lib/featury/resources/resource.rb +26 -0
- data/lib/featury/service/base.rb +25 -0
- data/lib/featury/service/builder.rb +8 -0
- data/lib/featury/service/exceptions.rb +13 -0
- data/lib/featury/version.rb +1 -1
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca75356ade329cfb662e46fa37ad0a80dbedb2deb0a58f23861f1e231d7e5425
|
4
|
+
data.tar.gz: 437cb00d9eb4eb07609df3f0c25ec2619af10c9264ed254d6c85f3eb0e527974
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d023c6922b92d82278313c4d6379a8e624d76f0e7972155558e5630516bdee079f24a096e1b565f71d6d98d7ecfc37c9238aa849d4efe68809c947e03a25ed5
|
7
|
+
data.tar.gz: cabb3d61981e2c44bce2c2d4ecbe1a5fa9d8857bedbd593060202368e08c5c9bbd4c1953ecadb9fbc4868124b49703c7a644e4d7fad293322f3ae4075b5c2d0b
|
data/README.md
CHANGED
@@ -3,9 +3,15 @@
|
|
3
3
|
<a href="https://github.com/servactory/featury/releases"><img src="https://img.shields.io/github/release-date/servactory/featury" alt="Release Date"></a>
|
4
4
|
</p>
|
5
5
|
|
6
|
-
##
|
6
|
+
## For what?
|
7
7
|
|
8
|
-
|
8
|
+
Featury is designed for grouping and managing multiple features in projects.
|
9
|
+
You can use any ready-made solution or your own.
|
10
|
+
Feature is easily customizable to suit projects and their goals.
|
11
|
+
|
12
|
+
[//]: # (## Documentation)
|
13
|
+
|
14
|
+
[//]: # (See [featury.servactory.com](https://featury.servactory.com) for documentation.)
|
9
15
|
|
10
16
|
## Quick Start
|
11
17
|
|
@@ -17,7 +23,105 @@ gem "featury"
|
|
17
23
|
|
18
24
|
### Usage
|
19
25
|
|
20
|
-
|
26
|
+
#### Basic class for your features
|
27
|
+
|
28
|
+
For example, you use Flipper for features.
|
29
|
+
In this case, the base class might look like this:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class ApplicationFeature < Featury::Base
|
33
|
+
action :enabled? do |features:|
|
34
|
+
features.all? { |feature| Flipper.enabled?(feature) }
|
35
|
+
end
|
36
|
+
|
37
|
+
action :disabled? do |features:|
|
38
|
+
features.any? { |feature| !Flipper.enabled?(feature) }
|
39
|
+
end
|
40
|
+
|
41
|
+
action :enable do |features:|
|
42
|
+
features.all? { |feature| Flipper.enable(feature) }
|
43
|
+
end
|
44
|
+
|
45
|
+
action :disable do |features:|
|
46
|
+
features.all? { |feature| Flipper.disable(feature) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
#### Features of your project
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class UserFeature::Onboarding < ApplicationFeature
|
55
|
+
resource :user, type: User
|
56
|
+
|
57
|
+
condition ->(resources:) { resources.user.onboarding_awaiting? }
|
58
|
+
|
59
|
+
prefix :user_onboarding
|
60
|
+
|
61
|
+
features(
|
62
|
+
:passage, # => :user_onboarding_passage
|
63
|
+
)
|
64
|
+
|
65
|
+
groups(
|
66
|
+
BillingFeature,
|
67
|
+
PaymentSystemFeature
|
68
|
+
)
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class BillingFeature < ApplicationFeature
|
74
|
+
prefix :billing
|
75
|
+
|
76
|
+
features(
|
77
|
+
:work # => :billing_work
|
78
|
+
)
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
class PaymentSystemFeature < ApplicationFeature
|
84
|
+
prefix :payment_system
|
85
|
+
|
86
|
+
features(
|
87
|
+
:work # => :payment_system_work
|
88
|
+
)
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
The `resource` method can indicate how the transmitted information should be processed.
|
93
|
+
In addition to the options that Servactory brings, there are options for specifying the processing mode of the transmitted data.
|
94
|
+
|
95
|
+
If it is necessary for a resource to be transferred as an option for a feature flag, then use the `option` option:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
resource :user, type: User, option: true
|
99
|
+
```
|
100
|
+
|
101
|
+
If it is necessary for a resource to be transferred to a nested group, then use the `nested` option:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
resource :user, type: User, nested: true
|
105
|
+
```
|
106
|
+
|
107
|
+
#### Working with the features of your project
|
108
|
+
|
109
|
+
Each of these actions will be applied to all feature flags.
|
110
|
+
And the result of these calls will be based on the result of all feature flags.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
UserFeature::Onboarding.enabled?(user:) # => true
|
114
|
+
UserFeature::Onboarding.disabled?(user:) # => false
|
115
|
+
UserFeature::Onboarding.enable(user:) # => true
|
116
|
+
UserFeature::Onboarding.disable(user:) # => false
|
117
|
+
```
|
118
|
+
|
119
|
+
If one of the feature flags is turned off, for example,
|
120
|
+
through your automation, then the main feature class will
|
121
|
+
return `false` when asked "is it enabled?".
|
122
|
+
|
123
|
+
In the example above, this could be the payment system and its shutdown due to technical work.
|
124
|
+
In this case, all onboarding of new users will be suspended.
|
21
125
|
|
22
126
|
## Contributing
|
23
127
|
|
@@ -0,0 +1,101 @@
|
|
1
|
+
en:
|
2
|
+
featury:
|
3
|
+
common:
|
4
|
+
undefined_method:
|
5
|
+
missing_name: "[%{service_class_name}] %{error_text}"
|
6
|
+
methods:
|
7
|
+
call:
|
8
|
+
not_used: "[%{service_class_name}] Nothing to perform. Use `make` or create a `call` method."
|
9
|
+
cannot_be_overwritten: "[%{service_class_name}] The following methods cannot be overwritten: %{list_of_methods}"
|
10
|
+
inputs:
|
11
|
+
undefined:
|
12
|
+
getter: "[%{service_class_name}] Undefined resource attribute `%{input_name}`"
|
13
|
+
setter: "[%{service_class_name}] Undefined resource attribute `%{input_name}`"
|
14
|
+
validations:
|
15
|
+
inclusion:
|
16
|
+
default_error: "[%{service_class_name}] Wrong value in `%{input_name}`, must be one of `%{input_inclusion}`"
|
17
|
+
must:
|
18
|
+
default_error: "[%{service_class_name}] Resource `%{input_name}` must \"%{code}\""
|
19
|
+
syntax_error: "[%{service_class_name}] Syntax error inside `%{code}` of `%{input_name}` resource: %{exception_message}"
|
20
|
+
dynamic_options:
|
21
|
+
consists_of:
|
22
|
+
required: "[%{service_class_name}] Required element in resource collection `%{input_name}` is missing"
|
23
|
+
wrong_type: "[%{service_class_name}] Wrong resource collection type `%{input_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
24
|
+
wrong_element_type: "[%{service_class_name}] Wrong element type in resource collection `%{input_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
25
|
+
format:
|
26
|
+
default: "[%{service_class_name}] Resource `%{input_name}` does not match `%{format_name}` format"
|
27
|
+
wrong_pattern: "[%{service_class_name}] Resource `%{input_name}` does not match `%{format_name}` format"
|
28
|
+
unknown: "[%{service_class_name}] Unknown `%{format_name}` format specified for resource `%{input_name}`"
|
29
|
+
min:
|
30
|
+
default: "[%{service_class_name}] Resource `%{input_name}` received value `%{value}`, which is less than `%{option_value}`"
|
31
|
+
max:
|
32
|
+
default: "[%{service_class_name}] Resource `%{input_name}` received value `%{value}`, which is greater than `%{option_value}`"
|
33
|
+
required:
|
34
|
+
default_error:
|
35
|
+
default: "[%{service_class_name}] Required resource `%{input_name}` is missing"
|
36
|
+
type:
|
37
|
+
default_error:
|
38
|
+
default: "[%{service_class_name}] Wrong type of resource `%{input_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
39
|
+
for_hash:
|
40
|
+
wrong_element_type: "[%{service_class_name}] Wrong type in resource hash `%{input_name}`, expected `%{expected_type}` for `%{key_name}`, got `%{given_type}`"
|
41
|
+
tools:
|
42
|
+
find_unnecessary:
|
43
|
+
error: "[%{service_class_name}] Unexpected attributes: `%{unnecessary_attributes}`"
|
44
|
+
rules:
|
45
|
+
error: "[%{service_class_name}] Conflict in `%{input_name}` resource options: `%{conflict_code}`"
|
46
|
+
internals:
|
47
|
+
undefined:
|
48
|
+
getter: "[%{service_class_name}] Undefined internal attribute `%{internal_name}`"
|
49
|
+
setter: "[%{service_class_name}] Undefined internal attribute `%{internal_name}`"
|
50
|
+
validations:
|
51
|
+
inclusion:
|
52
|
+
default_error: "[%{service_class_name}] Wrong value in `%{internal_name}`, must be one of `%{internal_inclusion}`"
|
53
|
+
must:
|
54
|
+
default_error: "[%{service_class_name}] Internal attribute `%{internal_name}` must \"%{code}\""
|
55
|
+
syntax_error: "[%{service_class_name}] Syntax error inside `%{code}` of `%{internal_name}` internal attribute: %{exception_message}"
|
56
|
+
dynamic_options:
|
57
|
+
consists_of:
|
58
|
+
required: "[%{service_class_name}] Required element in internal attribute collection `%{internal_name}` is missing"
|
59
|
+
wrong_type: "[%{service_class_name}] Wrong internal attribute collection type `%{internal_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
60
|
+
wrong_element_type: "[%{service_class_name}] Wrong element type in internal attribute collection `%{internal_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
61
|
+
format:
|
62
|
+
default: "[%{service_class_name}] Internal attribute `%{internal_name}` does not match `%{format_name}` format"
|
63
|
+
wrong_pattern: "[%{service_class_name}] Internal attribute `%{internal_name}` does not match `%{format_name}` format"
|
64
|
+
unknown: "[%{service_class_name}] Unknown `%{format_name}` format specified for internal attribute `%{internal_name}`"
|
65
|
+
min:
|
66
|
+
default: "[%{service_class_name}] Internal attribute `%{internal_name}` received value `%{value}`, which is less than `%{option_value}`"
|
67
|
+
max:
|
68
|
+
default: "[%{service_class_name}] Internal attribute `%{internal_name}` received value `%{value}`, which is greater than `%{option_value}`"
|
69
|
+
type:
|
70
|
+
default_error:
|
71
|
+
default: "[%{service_class_name}] Wrong type of internal attribute `%{internal_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
72
|
+
for_hash:
|
73
|
+
wrong_element_type: "[%{service_class_name}] Wrong type in internal attribute hash `%{internal_name}`, expected `%{expected_type}` for `%{key_name}`, got `%{given_type}`"
|
74
|
+
outputs:
|
75
|
+
undefined:
|
76
|
+
getter: "[%{service_class_name}] Undefined output attribute `%{output_name}`"
|
77
|
+
setter: "[%{service_class_name}] Undefined output attribute `%{output_name}`"
|
78
|
+
validations:
|
79
|
+
inclusion:
|
80
|
+
default_error: "[%{service_class_name}] Wrong value in `%{output_name}`, must be one of `%{output_inclusion}`"
|
81
|
+
must:
|
82
|
+
default_error: "[%{service_class_name}] Output attribute `%{output_name}` must \"%{code}\""
|
83
|
+
syntax_error: "[%{service_class_name}] Syntax error inside `%{code}` of `%{output_name}` output attribute: %{exception_message}"
|
84
|
+
dynamic_options:
|
85
|
+
consists_of:
|
86
|
+
required: "[%{service_class_name}] Required element in output attribute collection `%{output_name}` is missing"
|
87
|
+
wrong_type: "[%{service_class_name}] Wrong output attribute collection type `%{output_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
88
|
+
wrong_element_type: "[%{service_class_name}] Wrong element type in output attribute collection `%{output_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
89
|
+
format:
|
90
|
+
default: "[%{service_class_name}] Output attribute `%{output_name}` does not match `%{format_name}` format"
|
91
|
+
wrong_pattern: "[%{service_class_name}] Output attribute `%{output_name}` does not match `%{format_name}` format"
|
92
|
+
unknown: "[%{service_class_name}] Unknown `%{format_name}` format specified for output attribute `%{output_name}`"
|
93
|
+
min:
|
94
|
+
default: "[%{service_class_name}] Output attribute `%{output_name}` received value `%{value}`, which is less than `%{option_value}`"
|
95
|
+
max:
|
96
|
+
default: "[%{service_class_name}] Output attribute `%{output_name}` received value `%{value}`, which is greater than `%{option_value}`"
|
97
|
+
type:
|
98
|
+
default_error:
|
99
|
+
default: "[%{service_class_name}] Wrong type of output attribute `%{output_name}`, expected `%{expected_type}`, got `%{given_type}`"
|
100
|
+
for_hash:
|
101
|
+
wrong_element_type: "[%{service_class_name}] Wrong type in output attribute hash `%{output_name}`, expected `%{expected_type}` for `%{key_name}`, got `%{given_type}`"
|
@@ -0,0 +1,101 @@
|
|
1
|
+
ru:
|
2
|
+
featury:
|
3
|
+
common:
|
4
|
+
undefined_method:
|
5
|
+
missing_name: "[%{service_class_name}] %{error_text}"
|
6
|
+
methods:
|
7
|
+
call:
|
8
|
+
not_used: "[%{service_class_name}] Нечего выполнять. Используйте `make` или создайте метод `call`."
|
9
|
+
cannot_be_overwritten: "[%{service_class_name}] Нельзя перезаписать следующие методы: %{list_of_methods}"
|
10
|
+
inputs:
|
11
|
+
undefined:
|
12
|
+
getter: "[%{service_class_name}] Неизвестный входящий атрибут `%{input_name}`"
|
13
|
+
setter: "[%{service_class_name}] Неизвестный входящий атрибут `%{input_name}`"
|
14
|
+
validations:
|
15
|
+
inclusion:
|
16
|
+
default_error: "[%{service_class_name}] Неправильное значение в `%{input_name}`, должно быть одним из `%{input_inclusion}`"
|
17
|
+
must:
|
18
|
+
default_error: "[%{service_class_name}] Ресурс `%{input_name}` должен \"%{code}\""
|
19
|
+
syntax_error: "[%{service_class_name}] Синтаксическая ошибка внутри `%{code}` ресурса `%{input_name}`: %{exception_message}"
|
20
|
+
dynamic_options:
|
21
|
+
consists_of:
|
22
|
+
required: "[%{service_class_name}] Отсутствует обязательный элемент в коллекции ресурса `%{input_name}`"
|
23
|
+
wrong_type: "[%{service_class_name}] Неправильный тип коллекции ресурса `%{input_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
24
|
+
wrong_element_type: "[%{service_class_name}] Неправильный тип элемента в коллекции ресурса `%{input_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
25
|
+
format:
|
26
|
+
default: "[%{service_class_name}] Ресурс `%{input_name}` не соответствует формату `%{format_name}`"
|
27
|
+
wrong_pattern: "[%{service_class_name}] Ресурс `%{input_name}` не соответствует формату `%{format_name}`"
|
28
|
+
unknown: "[%{service_class_name}] Указан неизвестный формат `%{format_name}` у ресурса `%{input_name}`"
|
29
|
+
min:
|
30
|
+
default: "[%{service_class_name}] Ресурс `%{input_name}` получил значение `%{value}`, которое меньше `%{option_value}`"
|
31
|
+
max:
|
32
|
+
default: "[%{service_class_name}] Ресурс `%{input_name}` получил значение `%{value}`, которое больше `%{option_value}`"
|
33
|
+
required:
|
34
|
+
default_error:
|
35
|
+
default: "[%{service_class_name}] Обязательный ресурс `%{input_name}` отсутствует"
|
36
|
+
type:
|
37
|
+
default_error:
|
38
|
+
default: "[%{service_class_name}] Неправильный тип ресурса `%{input_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
39
|
+
for_hash:
|
40
|
+
wrong_element_type: "[%{service_class_name}] Неправильный тип в хеше ресурса `%{input_name}`, для `%{key_name}` ожидалось `%{expected_type}`, получено `%{given_type}`"
|
41
|
+
tools:
|
42
|
+
find_unnecessary:
|
43
|
+
error: "[%{service_class_name}] Неожиданные атрибуты: `%{unnecessary_attributes}`"
|
44
|
+
rules:
|
45
|
+
error: "[%{service_class_name}] Конфликт в опциях ресурса `%{input_name}`: `%{conflict_code}`"
|
46
|
+
internals:
|
47
|
+
undefined:
|
48
|
+
getter: "[%{service_class_name}] Неизвестный внутренний атрибут `%{internal_name}`"
|
49
|
+
setter: "[%{service_class_name}] Неизвестный внутренний атрибут `%{internal_name}`"
|
50
|
+
validations:
|
51
|
+
inclusion:
|
52
|
+
default_error: "[%{service_class_name}] Неправильное значение в `%{internal_name}`, должно быть одним из `%{internal_inclusion}`"
|
53
|
+
must:
|
54
|
+
default_error: "[%{service_class_name}] Внутренний атрибут `%{internal_name}` должен \"%{code}\""
|
55
|
+
syntax_error: "[%{service_class_name}] Синтаксическая ошибка внутри `%{code}` внутреннего атрибута `%{internal_name}`: %{exception_message}"
|
56
|
+
dynamic_options:
|
57
|
+
consists_of:
|
58
|
+
required: "[%{service_class_name}] Отсутствует обязательный элемент в коллекции внутреннего атрибута `%{internal_name}`"
|
59
|
+
wrong_type: "[%{service_class_name}] Неправильный тип коллекции внутреннего атрибута `%{internal_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
60
|
+
wrong_element_type: "[%{service_class_name}] Неправильный тип элемента в коллекции внутреннего атрибута `%{internal_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
61
|
+
format:
|
62
|
+
default: "[%{service_class_name}] Внутренний атрибут `%{internal_name}` не соответствует формату `%{format_name}`"
|
63
|
+
wrong_pattern: "[%{service_class_name}] Внутренний атрибут `%{internal_name}` не соответствует формату `%{format_name}`"
|
64
|
+
unknown: "[%{service_class_name}] Указан неизвестный формат `%{format_name}` у внутреннего атрибута `%{internal_name}`"
|
65
|
+
min:
|
66
|
+
default: "[%{service_class_name}] Внутренний атрибут `%{internal_name}` получил значение `%{value}`, которое меньше `%{option_value}`"
|
67
|
+
max:
|
68
|
+
default: "[%{service_class_name}] Внутренний атрибут `%{internal_name}` получил значение `%{value}`, которое больше `%{option_value}`"
|
69
|
+
type:
|
70
|
+
default_error:
|
71
|
+
default: "[%{service_class_name}] Неправильный тип внутреннего атрибута `%{internal_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
72
|
+
for_hash:
|
73
|
+
wrong_element_type: "[%{service_class_name}] Неправильный тип в хеше внутреннего атрибута `%{internal_name}`, для `%{key_name}` ожидалось `%{expected_type}`, получено `%{given_type}`"
|
74
|
+
outputs:
|
75
|
+
undefined:
|
76
|
+
getter: "[%{service_class_name}] Неизвестный выходящий атрибут `%{output_name}`"
|
77
|
+
setter: "[%{service_class_name}] Неизвестный выходящий атрибут `%{output_name}`"
|
78
|
+
validations:
|
79
|
+
inclusion:
|
80
|
+
default_error: "[%{service_class_name}] Неправильное значение в `%{output_name}`, должно быть одним из `%{output_inclusion}`"
|
81
|
+
must:
|
82
|
+
default_error: "[%{service_class_name}] Выходящий атрибут `%{output_name}` должен \"%{code}\""
|
83
|
+
syntax_error: "[%{service_class_name}] Синтаксическая ошибка внутри `%{code}` выходящего атрибута `%{output_name}`: %{exception_message}"
|
84
|
+
dynamic_options:
|
85
|
+
consists_of:
|
86
|
+
required: "[%{service_class_name}] Отсутствует обязательный элемент в коллекции выходящего атрибута `%{output_name}`"
|
87
|
+
wrong_type: "[%{service_class_name}] Неправильный тип коллекции выходящего атрибута `%{output_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
88
|
+
wrong_element_type: "[%{service_class_name}] Неправильный тип элемента в коллекции выходящего атрибута `%{output_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
89
|
+
format:
|
90
|
+
default: "[%{service_class_name}] Выходящий атрибут `%{output_name}` не соответствует формату `%{format_name}`"
|
91
|
+
wrong_pattern: "[%{service_class_name}] Выходящий атрибут `%{output_name}` не соответствует формату `%{format_name}`"
|
92
|
+
unknown: "[%{service_class_name}] Указан неизвестный формат `%{format_name}` у выходящего атрибута `%{output_name}`"
|
93
|
+
min:
|
94
|
+
default: "[%{service_class_name}] Выходящий атрибут `%{output_name}` получил значение `%{value}`, которое меньше `%{option_value}`"
|
95
|
+
max:
|
96
|
+
default: "[%{service_class_name}] Выходящий атрибут `%{output_name}` получил значение `%{value}`, которое больше `%{option_value}`"
|
97
|
+
type:
|
98
|
+
default_error:
|
99
|
+
default: "[%{service_class_name}] Неправильный тип выходящего атрибута `%{output_name}`, ожидалось `%{expected_type}`, получено `%{given_type}`"
|
100
|
+
for_hash:
|
101
|
+
wrong_element_type: "[%{service_class_name}] Неправильный тип в хеше выходящего атрибута `%{output_name}`, для `%{key_name}` ожидалось `%{expected_type}`, получено `%{given_type}`"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Actions
|
5
|
+
class Collection
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@collection, :<<, :each, :map, :merge, :find
|
8
|
+
|
9
|
+
def initialize(collection = Set.new)
|
10
|
+
@collection = collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def names
|
14
|
+
map(&:name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_by(name:)
|
18
|
+
find { |action| action.name == name }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Actions
|
5
|
+
module DSL
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
base.include(Workspace)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def inherited(child)
|
13
|
+
super
|
14
|
+
|
15
|
+
child.send(:collection_of_actions).merge(collection_of_actions)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def action(name)
|
21
|
+
collection_of_actions << Action.new(
|
22
|
+
name,
|
23
|
+
block: ->(features:, **options) { yield(features: features, **options) }
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def collection_of_actions
|
28
|
+
@collection_of_actions ||= Collection.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Actions
|
5
|
+
module Service
|
6
|
+
class Builder
|
7
|
+
SERVICE_CLASS_NAME = "SBuilder"
|
8
|
+
|
9
|
+
def self.build_and_call!(...)
|
10
|
+
new(...).build_and_call!
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
context:,
|
15
|
+
action:,
|
16
|
+
incoming_arguments:,
|
17
|
+
collection_of_resources:,
|
18
|
+
collection_of_conditions:,
|
19
|
+
collection_of_features:,
|
20
|
+
collection_of_groups:
|
21
|
+
)
|
22
|
+
@context = context
|
23
|
+
@action = action
|
24
|
+
@incoming_arguments = incoming_arguments
|
25
|
+
@collection_of_resources = collection_of_resources
|
26
|
+
@collection_of_conditions = collection_of_conditions
|
27
|
+
@collection_of_features = collection_of_features
|
28
|
+
@collection_of_groups = collection_of_groups
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_and_call!
|
32
|
+
Factory.create(@context.class, @collection_of_resources)
|
33
|
+
|
34
|
+
builder_class.call!(
|
35
|
+
action: @action,
|
36
|
+
**@incoming_arguments,
|
37
|
+
collection_of_resources: @collection_of_resources,
|
38
|
+
collection_of_conditions: @collection_of_conditions,
|
39
|
+
collection_of_features: @collection_of_features,
|
40
|
+
collection_of_groups: @collection_of_groups
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def builder_class
|
47
|
+
"#{@context.class.name}::#{SERVICE_CLASS_NAME}".constantize
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Actions
|
5
|
+
module Service
|
6
|
+
class Factory
|
7
|
+
def self.create(...)
|
8
|
+
new(...).create
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(model_class, collection_of_resources)
|
12
|
+
@model_class = model_class
|
13
|
+
@collection_of_resources = collection_of_resources
|
14
|
+
end
|
15
|
+
|
16
|
+
def create
|
17
|
+
return if @model_class.const_defined?(Builder::SERVICE_CLASS_NAME)
|
18
|
+
|
19
|
+
class_sample = create_service_class
|
20
|
+
|
21
|
+
@model_class.const_set(Builder::SERVICE_CLASS_NAME, class_sample)
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_service_class # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
25
|
+
collection_of_resources = @collection_of_resources
|
26
|
+
|
27
|
+
Class.new(Featury::Service::Builder) do
|
28
|
+
collection_of_resources.each do |resource|
|
29
|
+
input resource.name, **resource.options
|
30
|
+
end
|
31
|
+
|
32
|
+
input :action, type: Featury::Actions::Action
|
33
|
+
|
34
|
+
input :collection_of_resources, type: Featury::Resources::Collection
|
35
|
+
input :collection_of_conditions, type: Featury::Conditions::Collection
|
36
|
+
input :collection_of_features, type: Featury::Features::Collection
|
37
|
+
input :collection_of_groups, type: Featury::Groups::Collection
|
38
|
+
|
39
|
+
internal :conditions_are_true, type: [TrueClass, FalseClass]
|
40
|
+
internal :features_are_true, type: [TrueClass, FalseClass]
|
41
|
+
internal :groups_are_true, type: [TrueClass, FalseClass]
|
42
|
+
|
43
|
+
output :all_true, type: [TrueClass, FalseClass]
|
44
|
+
|
45
|
+
check :conditions
|
46
|
+
check :features
|
47
|
+
check :groups
|
48
|
+
|
49
|
+
check :all
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def check_conditions
|
54
|
+
internals.conditions_are_true = inputs.collection_of_conditions.all? do |condition|
|
55
|
+
condition.block.call(resources: inputs)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_features
|
60
|
+
options = inputs.collection_of_resources.only_option.to_h do |resource|
|
61
|
+
[resource.name, inputs.public_send(resource.name)]
|
62
|
+
end
|
63
|
+
|
64
|
+
internals.features_are_true =
|
65
|
+
inputs.action.block.call(features: inputs.collection_of_features.list, **options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_groups
|
69
|
+
arguments = inputs.collection_of_resources.only_nested.to_h do |resource|
|
70
|
+
[resource.name, inputs.public_send(resource.name)]
|
71
|
+
end
|
72
|
+
|
73
|
+
internals.groups_are_true = inputs.collection_of_groups.all? do |group|
|
74
|
+
group.group.public_send(inputs.action.name, **arguments)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_all
|
79
|
+
outputs.all_true =
|
80
|
+
internals.conditions_are_true &&
|
81
|
+
internals.features_are_true &&
|
82
|
+
internals.groups_are_true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Actions
|
5
|
+
module Workspace
|
6
|
+
private
|
7
|
+
|
8
|
+
def call!(
|
9
|
+
action:,
|
10
|
+
incoming_arguments:,
|
11
|
+
collection_of_resources:,
|
12
|
+
collection_of_conditions:,
|
13
|
+
collection_of_features:,
|
14
|
+
collection_of_groups:,
|
15
|
+
**
|
16
|
+
)
|
17
|
+
service_result = Service::Builder.build_and_call!(
|
18
|
+
context: self,
|
19
|
+
action: action,
|
20
|
+
incoming_arguments: incoming_arguments,
|
21
|
+
collection_of_resources: collection_of_resources,
|
22
|
+
collection_of_conditions: collection_of_conditions,
|
23
|
+
collection_of_features: collection_of_features,
|
24
|
+
collection_of_groups: collection_of_groups
|
25
|
+
)
|
26
|
+
|
27
|
+
super && service_result.success? && service_result.all_true?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/featury/base.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Conditions
|
5
|
+
class Collection
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@collection, :<<, :each, :merge, :all?
|
8
|
+
|
9
|
+
def initialize(collection = Set.new)
|
10
|
+
@collection = collection
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Conditions
|
5
|
+
module DSL
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def inherited(child)
|
12
|
+
super
|
13
|
+
|
14
|
+
child.send(:collection_of_conditions).merge(collection_of_conditions)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def condition(condition)
|
20
|
+
collection_of_conditions << Condition.new(condition)
|
21
|
+
end
|
22
|
+
|
23
|
+
def collection_of_conditions
|
24
|
+
@collection_of_conditions ||= Collection.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Context
|
5
|
+
module Callable
|
6
|
+
def method_missing(method_name, arguments = {}, &block)
|
7
|
+
action = collection_of_actions.find_by(name: method_name)
|
8
|
+
|
9
|
+
return super if action.nil?
|
10
|
+
|
11
|
+
context = send(:new)
|
12
|
+
|
13
|
+
_call!(context, action, **arguments)
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_missing?(method_name, *)
|
17
|
+
collection_of_actions.names.include?(method_name) || super
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def _call!(context, action, **arguments)
|
23
|
+
context.send(
|
24
|
+
:_call!,
|
25
|
+
action: action,
|
26
|
+
incoming_arguments: arguments.symbolize_keys,
|
27
|
+
collection_of_resources: collection_of_resources,
|
28
|
+
collection_of_conditions: collection_of_conditions,
|
29
|
+
collection_of_features: collection_of_features,
|
30
|
+
collection_of_groups: collection_of_groups
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Context
|
5
|
+
module Workspace
|
6
|
+
private
|
7
|
+
|
8
|
+
attr_reader :action,
|
9
|
+
:incoming_arguments,
|
10
|
+
:collection_of_conditions,
|
11
|
+
:collection_of_features,
|
12
|
+
:collection_of_groups
|
13
|
+
|
14
|
+
def _call!(
|
15
|
+
action:,
|
16
|
+
incoming_arguments:,
|
17
|
+
collection_of_resources:,
|
18
|
+
collection_of_conditions:,
|
19
|
+
collection_of_features:,
|
20
|
+
collection_of_groups:
|
21
|
+
)
|
22
|
+
call!(
|
23
|
+
action: action,
|
24
|
+
incoming_arguments: incoming_arguments,
|
25
|
+
collection_of_resources: collection_of_resources,
|
26
|
+
collection_of_conditions: collection_of_conditions,
|
27
|
+
collection_of_features: collection_of_features,
|
28
|
+
collection_of_groups: collection_of_groups
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def call!(
|
33
|
+
action:,
|
34
|
+
incoming_arguments:,
|
35
|
+
collection_of_resources:,
|
36
|
+
collection_of_conditions:,
|
37
|
+
collection_of_features:,
|
38
|
+
collection_of_groups:
|
39
|
+
)
|
40
|
+
@action = action
|
41
|
+
@incoming_arguments = incoming_arguments
|
42
|
+
@collection_of_resources = collection_of_resources
|
43
|
+
@collection_of_conditions = collection_of_conditions
|
44
|
+
@collection_of_features = collection_of_features
|
45
|
+
@collection_of_groups = collection_of_groups
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Features
|
5
|
+
class Collection
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@collection, :<<, :each, :map, :merge
|
8
|
+
|
9
|
+
def initialize(collection = Set.new)
|
10
|
+
@collection = collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def list
|
14
|
+
map { |feature| :"#{feature.prefix}_#{feature.name}" }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Features
|
5
|
+
module DSL
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def inherited(child)
|
12
|
+
super
|
13
|
+
|
14
|
+
child.send(:collection_of_features).merge(collection_of_features)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def prefix(prefix)
|
20
|
+
@prefix = prefix
|
21
|
+
end
|
22
|
+
|
23
|
+
def features(*names)
|
24
|
+
names.each do |name|
|
25
|
+
collection_of_features << Feature.new(@prefix, name)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def collection_of_features
|
30
|
+
@collection_of_features ||= Collection.new
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Groups
|
5
|
+
class Collection
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@collection, :<<, :each, :map, :filter, :to_h, :merge, :all?
|
8
|
+
|
9
|
+
def initialize(collection = Set.new)
|
10
|
+
@collection = collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def names
|
14
|
+
map(&:name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def internal_names
|
18
|
+
map { |attribute| attribute.to.name }
|
19
|
+
end
|
20
|
+
|
21
|
+
def include_class_exist?
|
22
|
+
@include_class_exist ||= filter do |attribute| # rubocop:disable Performance/Count
|
23
|
+
include_class = attribute.to.include_class
|
24
|
+
|
25
|
+
next false if include_class.nil?
|
26
|
+
|
27
|
+
if [Set, Array].include?(include_class)
|
28
|
+
include_class.any? { |item| item <= Datory::Base }
|
29
|
+
else
|
30
|
+
include_class <= Datory::Base
|
31
|
+
end
|
32
|
+
end.size.positive?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Groups
|
5
|
+
module DSL
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def inherited(child)
|
12
|
+
super
|
13
|
+
|
14
|
+
child.send(:collection_of_groups).merge(collection_of_groups)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def groups(*groups)
|
20
|
+
groups.each do |group|
|
21
|
+
collection_of_groups << Group.new(group)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def collection_of_groups
|
26
|
+
@collection_of_groups ||= Collection.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Resources
|
5
|
+
class Collection
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@collection, :<<, :each, :map, :filter, :to_h, :merge
|
8
|
+
|
9
|
+
def initialize(collection = Set.new)
|
10
|
+
@collection = collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def only_nested
|
14
|
+
Collection.new(filter(&:nested?))
|
15
|
+
end
|
16
|
+
|
17
|
+
def only_option
|
18
|
+
Collection.new(filter(&:option?))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Resources
|
5
|
+
module DSL
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def inherited(child)
|
12
|
+
super
|
13
|
+
|
14
|
+
child.send(:collection_of_resources).merge(collection_of_resources)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def resource(name, **options)
|
20
|
+
collection_of_resources << Resource.new(name, **options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def collection_of_resources
|
24
|
+
@collection_of_resources ||= Collection.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Resources
|
5
|
+
class Resource
|
6
|
+
attr_reader :name, :options
|
7
|
+
|
8
|
+
def initialize(name, **options)
|
9
|
+
@name = name
|
10
|
+
|
11
|
+
@nested = options.delete(:nested) || false
|
12
|
+
@option = options.delete(:option) || false
|
13
|
+
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def nested?
|
18
|
+
@nested
|
19
|
+
end
|
20
|
+
|
21
|
+
def option?
|
22
|
+
@option
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Service
|
5
|
+
class Base
|
6
|
+
include Servactory::DSL
|
7
|
+
|
8
|
+
configuration do
|
9
|
+
input_exception_class Featury::Service::Exceptions::Input
|
10
|
+
internal_exception_class Featury::Service::Exceptions::Internal
|
11
|
+
output_exception_class Featury::Service::Exceptions::Output
|
12
|
+
|
13
|
+
failure_class Featury::Service::Exceptions::Failure
|
14
|
+
|
15
|
+
result_class Featury::Expert
|
16
|
+
|
17
|
+
action_shortcuts %i[check]
|
18
|
+
|
19
|
+
i18n_root_key :featury
|
20
|
+
|
21
|
+
predicate_methods_enabled true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featury
|
4
|
+
module Service
|
5
|
+
module Exceptions
|
6
|
+
class Input < Servactory::Exceptions::Input; end
|
7
|
+
class Output < Servactory::Exceptions::Output; end
|
8
|
+
class Internal < Servactory::Exceptions::Internal; end
|
9
|
+
|
10
|
+
class Failure < Servactory::Exceptions::Failure; end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/featury/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: featury
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.rc3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anton Sokolov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-06-
|
11
|
+
date: 2024-06-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -193,8 +193,36 @@ extra_rdoc_files: []
|
|
193
193
|
files:
|
194
194
|
- README.md
|
195
195
|
- Rakefile
|
196
|
+
- config/locales/en.yml
|
197
|
+
- config/locales/ru.yml
|
196
198
|
- lib/featury.rb
|
199
|
+
- lib/featury/actions/action.rb
|
200
|
+
- lib/featury/actions/collection.rb
|
201
|
+
- lib/featury/actions/dsl.rb
|
202
|
+
- lib/featury/actions/service/builder.rb
|
203
|
+
- lib/featury/actions/service/factory.rb
|
204
|
+
- lib/featury/actions/workspace.rb
|
205
|
+
- lib/featury/base.rb
|
206
|
+
- lib/featury/conditions/collection.rb
|
207
|
+
- lib/featury/conditions/condition.rb
|
208
|
+
- lib/featury/conditions/dsl.rb
|
209
|
+
- lib/featury/context/callable.rb
|
210
|
+
- lib/featury/context/dsl.rb
|
211
|
+
- lib/featury/context/workspace.rb
|
197
212
|
- lib/featury/engine.rb
|
213
|
+
- lib/featury/expert.rb
|
214
|
+
- lib/featury/features/collection.rb
|
215
|
+
- lib/featury/features/dsl.rb
|
216
|
+
- lib/featury/features/feature.rb
|
217
|
+
- lib/featury/groups/collection.rb
|
218
|
+
- lib/featury/groups/dsl.rb
|
219
|
+
- lib/featury/groups/group.rb
|
220
|
+
- lib/featury/resources/collection.rb
|
221
|
+
- lib/featury/resources/dsl.rb
|
222
|
+
- lib/featury/resources/resource.rb
|
223
|
+
- lib/featury/service/base.rb
|
224
|
+
- lib/featury/service/builder.rb
|
225
|
+
- lib/featury/service/exceptions.rb
|
198
226
|
- lib/featury/version.rb
|
199
227
|
homepage: https://github.com/servactory/featury
|
200
228
|
licenses:
|