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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34b31d37fde692827b9794be55fbc6baaab7746942fb8dfa3b38db6dc7753cf8
4
- data.tar.gz: 0bcd93b20304bb5f72d05536bff183d746bd93afb8bf183baa1e1617c36bbff9
3
+ metadata.gz: ca75356ade329cfb662e46fa37ad0a80dbedb2deb0a58f23861f1e231d7e5425
4
+ data.tar.gz: 437cb00d9eb4eb07609df3f0c25ec2619af10c9264ed254d6c85f3eb0e527974
5
5
  SHA512:
6
- metadata.gz: 51fb82d2160af0f453a6fcc86a5640bbf71013d898340c504e4666c2195fb8c3b07ffb6780e2bd11da9b32453a9bf26e1528102be2ed884f610cd756e798aff6
7
- data.tar.gz: 4de9e147cc15fdcd42d795b524c9ccb85f90df874627998a8a4271cca99cc173c5958982ce364bbe9bbed7ec32c6f448c7a3819a2d9148ee0279a689da0b1e20
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
- ## 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,105 @@ 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
+ 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,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,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
@@ -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,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,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,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,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 = "rc3"
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.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-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: