featury 1.0.0.rc1 → 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34b31d37fde692827b9794be55fbc6baaab7746942fb8dfa3b38db6dc7753cf8
4
- data.tar.gz: 0bcd93b20304bb5f72d05536bff183d746bd93afb8bf183baa1e1617c36bbff9
3
+ metadata.gz: 8098675501d6667179f6362ab15e8b28667fec0cd365380e6f96940f60167a16
4
+ data.tar.gz: b462b0692a217a23db8729d3205012668d5955efe58cff0f586e460aa3f9133d
5
5
  SHA512:
6
- metadata.gz: 51fb82d2160af0f453a6fcc86a5640bbf71013d898340c504e4666c2195fb8c3b07ffb6780e2bd11da9b32453a9bf26e1528102be2ed884f610cd756e798aff6
7
- data.tar.gz: 4de9e147cc15fdcd42d795b524c9ccb85f90df874627998a8a4271cca99cc173c5958982ce364bbe9bbed7ec32c6f448c7a3819a2d9148ee0279a689da0b1e20
6
+ metadata.gz: 9a7e141130c232159477292ed179e8267fc50163f56b4c0d2cfe5b18238b2a7a498bbfd903d659bce143c447cb578c1ffa11e763a07988ecb3a6112286c17b71
7
+ data.tar.gz: 24ebf4c4d1802856b78683917c5a717c4180823214e3dfbcbecefe30a53ae38f2558f46f09d7f18b395ebdd492091c6c8cc1fa2e3a61031c30e19d3bf2b0ba23
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
- ## Documentation
6
+ ## For what?
7
7
 
8
- See [featury.servactory.com](https://featury.servactory.com) for documentation.
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]&#40;https://featury.servactory.com&#41; for documentation.)
9
15
 
10
16
  ## Quick Start
11
17
 
@@ -17,7 +23,96 @@ gem "featury"
17
23
 
18
24
  ### Usage
19
25
 
20
- Soon
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
+ # The user resource is currently needed due to availability and passing from the parent class.
75
+ resource :user, type: User
76
+
77
+ prefix :billing
78
+
79
+ features(
80
+ :work # => :billing_work
81
+ )
82
+ end
83
+ ```
84
+
85
+ ```ruby
86
+ class PaymentSystemFeature < ApplicationFeature
87
+ # The user resource is currently needed due to availability and passing from the parent class.
88
+ resource :user, type: User
89
+
90
+ prefix :payment_system
91
+
92
+ features(
93
+ :work # => :payment_system_work
94
+ )
95
+ end
96
+ ```
97
+
98
+ #### Working with the features of your project
99
+
100
+ Each of these actions will be applied to all feature flags.
101
+ And the result of these calls will be based on the result of all feature flags.
102
+
103
+ ```ruby
104
+ UserFeature::Onboarding.enabled?(user:) # => true
105
+ UserFeature::Onboarding.disabled?(user:) # => false
106
+ UserFeature::Onboarding.enable(user:) # => true
107
+ UserFeature::Onboarding.disable(user:) # => false
108
+ ```
109
+
110
+ If one of the feature flags is turned off, for example,
111
+ through your automation, then the main feature class will
112
+ return `false` when asked "is it enabled?".
113
+
114
+ In the example above, this could be the payment system and its shutdown due to technical work.
115
+ In this case, all onboarding of new users will be suspended.
21
116
 
22
117
  ## Contributing
23
118
 
@@ -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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ module Actions
5
+ class Action
6
+ attr_reader :name, :block
7
+
8
+ def initialize(name, block)
9
+ @name = name
10
+ @block = block
11
+ end
12
+ end
13
+ end
14
+ end
@@ -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,30 @@
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(name, ->(features:) { yield(features: features) })
22
+ end
23
+
24
+ def collection_of_actions
25
+ @collection_of_actions ||= Collection.new
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
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_conditions: @collection_of_conditions,
38
+ collection_of_features: @collection_of_features,
39
+ collection_of_groups: @collection_of_groups
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ def builder_class
46
+ "#{@context.class.name}::#{SERVICE_CLASS_NAME}".constantize
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,83 @@
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_conditions, type: Featury::Conditions::Collection
35
+ input :collection_of_features, type: Featury::Features::Collection
36
+ input :collection_of_groups, type: Featury::Groups::Collection
37
+
38
+ internal :conditions_are_true, type: [TrueClass, FalseClass]
39
+ internal :features_are_true, type: [TrueClass, FalseClass]
40
+ internal :groups_are_true, type: [TrueClass, FalseClass]
41
+
42
+ output :all_true, type: [TrueClass, FalseClass]
43
+
44
+ check :conditions
45
+ check :features
46
+ check :groups
47
+
48
+ check :all
49
+
50
+ private
51
+
52
+ def check_conditions
53
+ internals.conditions_are_true = inputs.collection_of_conditions.all? do |condition|
54
+ condition.block.call(resources: inputs)
55
+ end
56
+ end
57
+
58
+ def check_features
59
+ internals.features_are_true = inputs.action.block.call(features: inputs.collection_of_features.list)
60
+ end
61
+
62
+ def check_groups
63
+ arguments = inputs.instance_variable_get(:@collection_of_inputs).names.to_h do |input_name|
64
+ [input_name, inputs.public_send(input_name)]
65
+ end
66
+
67
+ internals.groups_are_true = inputs.collection_of_groups.all? do |group|
68
+ group.group.public_send(inputs.action.name, **arguments)
69
+ end
70
+ end
71
+
72
+ def check_all
73
+ outputs.all_true =
74
+ internals.conditions_are_true &&
75
+ internals.features_are_true &&
76
+ internals.groups_are_true
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ 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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ class Base
5
+ include Context::DSL
6
+ include Actions::DSL
7
+ include Groups::DSL
8
+ include Features::DSL
9
+ include Conditions::DSL
10
+ include Resources::DSL
11
+ end
12
+ end
@@ -0,0 +1,36 @@
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
+
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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ module Conditions
5
+ class Condition
6
+ attr_reader :block
7
+
8
+ def initialize(block)
9
+ @block = block
10
+ end
11
+ end
12
+ end
13
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ module Context
5
+ module DSL
6
+ def self.included(base)
7
+ base.extend(Callable)
8
+ base.include(Workspace)
9
+ end
10
+ end
11
+ end
12
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ class Expert < Servactory::Result; end
5
+ 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ module Features
5
+ class Feature
6
+ attr_reader :prefix, :name
7
+
8
+ def initialize(prefix, name)
9
+ @prefix = prefix
10
+ @name = name
11
+ end
12
+ end
13
+ end
14
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ module Groups
5
+ class Group
6
+ attr_reader :group
7
+
8
+ def initialize(group)
9
+ @group = group
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,36 @@
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 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,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,14 @@
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
+ @options = options
11
+ end
12
+ end
13
+ end
14
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featury
4
+ module Service
5
+ class Builder < Base
6
+ end
7
+ end
8
+ 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
@@ -5,7 +5,7 @@ module Featury
5
5
  MAJOR = 1
6
6
  MINOR = 0
7
7
  PATCH = 0
8
- PRE = "rc1"
8
+ PRE = "rc2"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
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.rc1
4
+ version: 1.0.0.rc2
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-20 00:00:00.000000000 Z
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: