subflag-rails 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +202 -39
- data/app/controllers/subflag/rails/application_controller.rb +22 -0
- data/app/controllers/subflag/rails/flags_controller.rb +85 -0
- data/app/views/layouts/subflag/rails/application.html.erb +72 -0
- data/app/views/subflag/rails/flags/_form.html.erb +45 -0
- data/app/views/subflag/rails/flags/edit.html.erb +241 -0
- data/app/views/subflag/rails/flags/index.html.erb +50 -0
- data/app/views/subflag/rails/flags/new.html.erb +5 -0
- data/config/routes.rb +12 -0
- data/lib/generators/subflag/install_generator.rb +9 -1
- data/lib/generators/subflag/templates/create_subflag_flags.rb.tt +5 -0
- data/lib/subflag/rails/backends/active_record_provider.rb +49 -18
- data/lib/subflag/rails/configuration.rb +23 -0
- data/lib/subflag/rails/engine.rb +34 -0
- data/lib/subflag/rails/models/flag.rb +106 -17
- data/lib/subflag/rails/targeting.rb +48 -0
- data/lib/subflag/rails/targeting_engine.rb +191 -0
- data/lib/subflag/rails/version.rb +1 -1
- data/lib/subflag/rails.rb +2 -0
- metadata +14 -3
|
@@ -2,27 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
module Subflag
|
|
4
4
|
module Rails
|
|
5
|
-
# ActiveRecord model for storing feature flags in your database
|
|
5
|
+
# ActiveRecord model for storing feature flags in your database.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# Supports targeting rules to show different values to different users.
|
|
8
|
+
# Perfect for internal testing before wider rollout.
|
|
9
|
+
#
|
|
10
|
+
# @example Simple flag (everyone gets the same value)
|
|
8
11
|
# Subflag::Rails::Flag.create!(
|
|
9
12
|
# key: "new-checkout",
|
|
10
13
|
# value: "true",
|
|
11
14
|
# value_type: "boolean"
|
|
12
15
|
# )
|
|
13
16
|
#
|
|
14
|
-
# @example
|
|
17
|
+
# @example Flag with targeting rules (internal team gets different value)
|
|
15
18
|
# Subflag::Rails::Flag.create!(
|
|
16
|
-
# key: "
|
|
17
|
-
# value: "
|
|
18
|
-
# value_type: "
|
|
19
|
+
# key: "new-dashboard",
|
|
20
|
+
# value: "false",
|
|
21
|
+
# value_type: "boolean",
|
|
22
|
+
# targeting_rules: [
|
|
23
|
+
# {
|
|
24
|
+
# "value" => "true",
|
|
25
|
+
# "conditions" => {
|
|
26
|
+
# "type" => "OR",
|
|
27
|
+
# "conditions" => [
|
|
28
|
+
# { "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@company.com" },
|
|
29
|
+
# { "attribute" => "role", "operator" => "IN", "value" => ["admin", "developer", "qa"] }
|
|
30
|
+
# ]
|
|
31
|
+
# }
|
|
32
|
+
# }
|
|
33
|
+
# ]
|
|
19
34
|
# )
|
|
20
35
|
#
|
|
21
|
-
# @example
|
|
36
|
+
# @example Progressive rollout (first match wins)
|
|
22
37
|
# Subflag::Rails::Flag.create!(
|
|
23
|
-
# key: "
|
|
24
|
-
# value:
|
|
25
|
-
# value_type: "
|
|
38
|
+
# key: "max-projects",
|
|
39
|
+
# value: "5",
|
|
40
|
+
# value_type: "integer",
|
|
41
|
+
# targeting_rules: [
|
|
42
|
+
# { "value" => "1000", "conditions" => { "type" => "AND", "conditions" => [{ "attribute" => "role", "operator" => "EQUALS", "value" => "admin" }] } },
|
|
43
|
+
# { "value" => "100", "conditions" => { "type" => "AND", "conditions" => [{ "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@company.com" }] } },
|
|
44
|
+
# { "value" => "25", "conditions" => { "type" => "AND", "conditions" => [{ "attribute" => "plan", "operator" => "EQUALS", "value" => "pro" }] } }
|
|
45
|
+
# ]
|
|
26
46
|
# )
|
|
27
47
|
#
|
|
28
48
|
class Flag < ::ActiveRecord::Base
|
|
@@ -35,29 +55,98 @@ module Subflag
|
|
|
35
55
|
format: { with: /\A[a-z0-9\-]+\z/, message: "only lowercase letters, numbers, and dashes" }
|
|
36
56
|
validates :value_type, inclusion: { in: VALUE_TYPES }
|
|
37
57
|
validates :value, presence: true
|
|
58
|
+
validate :validate_targeting_rules
|
|
38
59
|
|
|
39
60
|
scope :enabled, -> { where(enabled: true) }
|
|
40
61
|
|
|
41
|
-
#
|
|
62
|
+
# Evaluate the flag for a given context
|
|
63
|
+
#
|
|
64
|
+
# Returns the matched rule's value if context matches targeting rules,
|
|
65
|
+
# otherwise returns the default value.
|
|
66
|
+
#
|
|
67
|
+
# @param context [Hash, nil] Evaluation context with user attributes
|
|
68
|
+
# @param expected_type [Symbol, String, nil] Override the value_type for casting
|
|
69
|
+
# @return [Object] The evaluated value, cast to the appropriate type
|
|
70
|
+
def evaluate(context: nil, expected_type: nil)
|
|
71
|
+
rules = parsed_targeting_rules
|
|
72
|
+
raw_value = if rules.present? && context.present?
|
|
73
|
+
matched = TargetingEngine.evaluate(rules, context)
|
|
74
|
+
matched || value
|
|
75
|
+
else
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
cast_value(raw_value, expected_type)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get the flag's default value cast to its declared type (ignores targeting)
|
|
42
83
|
#
|
|
43
84
|
# @param expected_type [Symbol, String, nil] Override the value_type for casting
|
|
44
85
|
# @return [Object] The typed value
|
|
45
86
|
def typed_value(expected_type = nil)
|
|
87
|
+
cast_value(value, expected_type)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def cast_value(raw_value, expected_type = nil)
|
|
46
93
|
type = expected_type&.to_s || value_type
|
|
47
94
|
|
|
48
95
|
case type.to_s
|
|
49
96
|
when "boolean"
|
|
50
|
-
ActiveModel::Type::Boolean.new.cast(
|
|
97
|
+
ActiveModel::Type::Boolean.new.cast(raw_value)
|
|
51
98
|
when "string"
|
|
52
|
-
|
|
99
|
+
raw_value.to_s
|
|
53
100
|
when "integer"
|
|
54
|
-
|
|
101
|
+
raw_value.to_i
|
|
55
102
|
when "float", "number"
|
|
56
|
-
|
|
103
|
+
raw_value.to_f
|
|
57
104
|
when "object"
|
|
58
|
-
|
|
105
|
+
raw_value.is_a?(Hash) ? raw_value : JSON.parse(raw_value)
|
|
59
106
|
else
|
|
60
|
-
|
|
107
|
+
raw_value
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parsed_targeting_rules
|
|
112
|
+
return nil if targeting_rules.blank?
|
|
113
|
+
|
|
114
|
+
case targeting_rules
|
|
115
|
+
when String
|
|
116
|
+
JSON.parse(targeting_rules)
|
|
117
|
+
when Array
|
|
118
|
+
targeting_rules
|
|
119
|
+
else
|
|
120
|
+
targeting_rules
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_targeting_rules
|
|
125
|
+
return if targeting_rules.blank?
|
|
126
|
+
|
|
127
|
+
rules = case targeting_rules
|
|
128
|
+
when Array then targeting_rules
|
|
129
|
+
when String
|
|
130
|
+
begin
|
|
131
|
+
JSON.parse(targeting_rules)
|
|
132
|
+
rescue JSON::ParserError
|
|
133
|
+
errors.add(:targeting_rules, "must be valid JSON")
|
|
134
|
+
return
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
errors.add(:targeting_rules, "must be an array of rules")
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
rules.each_with_index do |rule, index|
|
|
142
|
+
unless rule.is_a?(Hash)
|
|
143
|
+
errors.add(:targeting_rules, "rule #{index} must be a hash")
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
rule = rule.transform_keys(&:to_s)
|
|
148
|
+
errors.add(:targeting_rules, "rule #{index} must have a 'value' key") unless rule.key?("value")
|
|
149
|
+
errors.add(:targeting_rules, "rule #{index} must have a 'conditions' key") unless rule.key?("conditions")
|
|
61
150
|
end
|
|
62
151
|
end
|
|
63
152
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "targeting_engine"
|
|
4
|
+
|
|
5
|
+
module Subflag
|
|
6
|
+
module Rails
|
|
7
|
+
# Targeting module for evaluating rules that control who sees what flag values.
|
|
8
|
+
#
|
|
9
|
+
# Rules are stored as JSON in the database and evaluated at runtime.
|
|
10
|
+
# Use targeting to roll out features to internal teams before wider release.
|
|
11
|
+
#
|
|
12
|
+
# ## Rule Format
|
|
13
|
+
#
|
|
14
|
+
# Rules are an array of objects, each with a `value` and `conditions`:
|
|
15
|
+
#
|
|
16
|
+
# [
|
|
17
|
+
# {
|
|
18
|
+
# "value" => "true",
|
|
19
|
+
# "conditions" => {
|
|
20
|
+
# "type" => "OR",
|
|
21
|
+
# "conditions" => [
|
|
22
|
+
# { "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@company.com" },
|
|
23
|
+
# { "attribute" => "role", "operator" => "IN", "value" => ["admin", "qa"] }
|
|
24
|
+
# ]
|
|
25
|
+
# }
|
|
26
|
+
# }
|
|
27
|
+
# ]
|
|
28
|
+
#
|
|
29
|
+
# ## Supported Operators
|
|
30
|
+
#
|
|
31
|
+
# - EQUALS, NOT_EQUALS - exact match
|
|
32
|
+
# - IN, NOT_IN - list membership
|
|
33
|
+
# - CONTAINS, NOT_CONTAINS - substring match
|
|
34
|
+
# - STARTS_WITH, ENDS_WITH - prefix/suffix match
|
|
35
|
+
# - GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL - numeric
|
|
36
|
+
# - MATCHES - regex match
|
|
37
|
+
#
|
|
38
|
+
# ## Evaluation
|
|
39
|
+
#
|
|
40
|
+
# Rules are evaluated in order. First match wins.
|
|
41
|
+
# If no rules match, the flag's default value is returned.
|
|
42
|
+
#
|
|
43
|
+
# TODO: Add admin UI for managing targeting rules
|
|
44
|
+
#
|
|
45
|
+
module Targeting
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
module Rails
|
|
5
|
+
# Evaluates targeting rules against evaluation contexts.
|
|
6
|
+
#
|
|
7
|
+
# Rules are arrays of { "value" => X, "conditions" => {...} } hashes.
|
|
8
|
+
# First matching rule wins. Falls back to flag's default value if nothing matches.
|
|
9
|
+
#
|
|
10
|
+
# @example Rule structure
|
|
11
|
+
# [
|
|
12
|
+
# {
|
|
13
|
+
# "value" => "100",
|
|
14
|
+
# "conditions" => {
|
|
15
|
+
# "type" => "OR",
|
|
16
|
+
# "conditions" => [
|
|
17
|
+
# { "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@company.com" },
|
|
18
|
+
# { "attribute" => "role", "operator" => "IN", "value" => ["admin", "developer"] }
|
|
19
|
+
# ]
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
# ]
|
|
23
|
+
#
|
|
24
|
+
module TargetingEngine
|
|
25
|
+
OPERATORS = %w[
|
|
26
|
+
EQUALS NOT_EQUALS IN NOT_IN
|
|
27
|
+
CONTAINS NOT_CONTAINS STARTS_WITH ENDS_WITH
|
|
28
|
+
GREATER_THAN LESS_THAN GREATER_THAN_OR_EQUAL LESS_THAN_OR_EQUAL
|
|
29
|
+
MATCHES
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Evaluate targeting rules against a context
|
|
34
|
+
#
|
|
35
|
+
# @param rules [Array<Hash>, nil] Array of targeting rules with values
|
|
36
|
+
# @param context [Hash, nil] Evaluation context with attributes
|
|
37
|
+
# @return [String, nil] The matched rule's value, or nil if no match
|
|
38
|
+
def evaluate(rules, context)
|
|
39
|
+
return nil if rules.nil? || rules.empty?
|
|
40
|
+
return nil if context.nil? || context.empty?
|
|
41
|
+
|
|
42
|
+
# Normalize context keys to strings for comparison
|
|
43
|
+
normalized_context = normalize_context(context)
|
|
44
|
+
|
|
45
|
+
rules.each do |rule|
|
|
46
|
+
rule = rule.transform_keys(&:to_s)
|
|
47
|
+
conditions = rule["conditions"]
|
|
48
|
+
next unless conditions
|
|
49
|
+
|
|
50
|
+
if evaluate_rule(conditions, normalized_context)
|
|
51
|
+
return rule["value"]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
nil # No rules matched
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def normalize_context(context)
|
|
61
|
+
context.transform_keys(&:to_s).transform_values do |v|
|
|
62
|
+
v.is_a?(Symbol) ? v.to_s : v
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Evaluate an AND/OR rule block
|
|
67
|
+
def evaluate_rule(rule, context)
|
|
68
|
+
rule = rule.transform_keys(&:to_s)
|
|
69
|
+
type = rule["type"]&.upcase || "AND"
|
|
70
|
+
conditions = rule["conditions"] || []
|
|
71
|
+
|
|
72
|
+
case type
|
|
73
|
+
when "AND"
|
|
74
|
+
conditions.all? { |c| evaluate_condition(c, context) }
|
|
75
|
+
when "OR"
|
|
76
|
+
conditions.any? { |c| evaluate_condition(c, context) }
|
|
77
|
+
else
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Evaluate a single condition
|
|
83
|
+
def evaluate_condition(condition, context)
|
|
84
|
+
condition = condition.transform_keys(&:to_s)
|
|
85
|
+
attribute = condition["attribute"]
|
|
86
|
+
operator = condition["operator"]&.upcase
|
|
87
|
+
expected = condition["value"]
|
|
88
|
+
|
|
89
|
+
return false unless attribute && operator
|
|
90
|
+
|
|
91
|
+
actual = context[attribute]
|
|
92
|
+
return false if actual.nil?
|
|
93
|
+
|
|
94
|
+
case operator
|
|
95
|
+
when "EQUALS"
|
|
96
|
+
compare_equals(actual, expected)
|
|
97
|
+
when "NOT_EQUALS"
|
|
98
|
+
!compare_equals(actual, expected)
|
|
99
|
+
when "IN"
|
|
100
|
+
compare_in(actual, expected)
|
|
101
|
+
when "NOT_IN"
|
|
102
|
+
!compare_in(actual, expected)
|
|
103
|
+
when "CONTAINS"
|
|
104
|
+
compare_contains(actual, expected)
|
|
105
|
+
when "NOT_CONTAINS"
|
|
106
|
+
!compare_contains(actual, expected)
|
|
107
|
+
when "STARTS_WITH"
|
|
108
|
+
compare_starts_with(actual, expected)
|
|
109
|
+
when "ENDS_WITH"
|
|
110
|
+
compare_ends_with(actual, expected)
|
|
111
|
+
when "GREATER_THAN"
|
|
112
|
+
compare_numeric(actual, expected) { |a, e| a > e }
|
|
113
|
+
when "LESS_THAN"
|
|
114
|
+
compare_numeric(actual, expected) { |a, e| a < e }
|
|
115
|
+
when "GREATER_THAN_OR_EQUAL"
|
|
116
|
+
compare_numeric(actual, expected) { |a, e| a >= e }
|
|
117
|
+
when "LESS_THAN_OR_EQUAL"
|
|
118
|
+
compare_numeric(actual, expected) { |a, e| a <= e }
|
|
119
|
+
when "MATCHES"
|
|
120
|
+
compare_matches(actual, expected)
|
|
121
|
+
else
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def compare_equals(actual, expected)
|
|
127
|
+
normalize_value(actual) == normalize_value(expected)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def compare_in(actual, expected)
|
|
131
|
+
return false unless expected.is_a?(Array)
|
|
132
|
+
|
|
133
|
+
normalized_actual = normalize_value(actual)
|
|
134
|
+
expected.any? { |e| normalize_value(e) == normalized_actual }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def compare_contains(actual, expected)
|
|
138
|
+
actual.to_s.include?(expected.to_s)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def compare_starts_with(actual, expected)
|
|
142
|
+
actual.to_s.start_with?(expected.to_s)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def compare_ends_with(actual, expected)
|
|
146
|
+
actual.to_s.end_with?(expected.to_s)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def compare_numeric(actual, expected)
|
|
150
|
+
a = to_number(actual)
|
|
151
|
+
e = to_number(expected)
|
|
152
|
+
return false if a.nil? || e.nil?
|
|
153
|
+
|
|
154
|
+
yield(a, e)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def compare_matches(actual, expected)
|
|
158
|
+
Regexp.new(expected.to_s).match?(actual.to_s)
|
|
159
|
+
rescue RegexpError
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def normalize_value(value)
|
|
164
|
+
case value
|
|
165
|
+
when Symbol then value.to_s
|
|
166
|
+
when TrueClass then true
|
|
167
|
+
when FalseClass then false
|
|
168
|
+
when Numeric then value
|
|
169
|
+
else value.to_s
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def to_number(value)
|
|
174
|
+
case value
|
|
175
|
+
when Numeric then value
|
|
176
|
+
when String
|
|
177
|
+
if value.include?(".")
|
|
178
|
+
Float(value)
|
|
179
|
+
else
|
|
180
|
+
Integer(value)
|
|
181
|
+
end
|
|
182
|
+
else
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
rescue ArgumentError, TypeError
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
data/lib/subflag/rails.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "rails/client"
|
|
|
11
11
|
require_relative "rails/flag_accessor"
|
|
12
12
|
require_relative "rails/helpers"
|
|
13
13
|
require_relative "rails/railtie" if defined?(Rails::Railtie)
|
|
14
|
+
require_relative "rails/engine" if defined?(Rails::Engine)
|
|
14
15
|
|
|
15
16
|
# Test helpers are loaded separately: require "subflag/rails/test_helpers"
|
|
16
17
|
|
|
@@ -130,6 +131,7 @@ module Subflag
|
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
def build_active_record_provider
|
|
134
|
+
require_relative "rails/targeting"
|
|
133
135
|
require_relative "rails/backends/active_record_provider"
|
|
134
136
|
require_relative "rails/models/flag"
|
|
135
137
|
Backends::ActiveRecordProvider.new
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: subflag-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Subflag
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: openfeature-sdk
|
|
@@ -170,7 +170,7 @@ dependencies:
|
|
|
170
170
|
- - ">="
|
|
171
171
|
- !ruby/object:Gem::Version
|
|
172
172
|
version: '6.1'
|
|
173
|
-
description: Feature flags for Rails with
|
|
173
|
+
description: Feature flags for Rails with selectable backends. Use Subflag Cloud (SaaS),
|
|
174
174
|
ActiveRecord (self-hosted), or Memory (testing). Get typed values (boolean, string,
|
|
175
175
|
integer, double, object) with the same API regardless of backend.
|
|
176
176
|
email:
|
|
@@ -182,6 +182,14 @@ files:
|
|
|
182
182
|
- CHANGELOG.md
|
|
183
183
|
- LICENSE.txt
|
|
184
184
|
- README.md
|
|
185
|
+
- app/controllers/subflag/rails/application_controller.rb
|
|
186
|
+
- app/controllers/subflag/rails/flags_controller.rb
|
|
187
|
+
- app/views/layouts/subflag/rails/application.html.erb
|
|
188
|
+
- app/views/subflag/rails/flags/_form.html.erb
|
|
189
|
+
- app/views/subflag/rails/flags/edit.html.erb
|
|
190
|
+
- app/views/subflag/rails/flags/index.html.erb
|
|
191
|
+
- app/views/subflag/rails/flags/new.html.erb
|
|
192
|
+
- config/routes.rb
|
|
185
193
|
- lib/generators/subflag/install_generator.rb
|
|
186
194
|
- lib/generators/subflag/templates/create_subflag_flags.rb.tt
|
|
187
195
|
- lib/generators/subflag/templates/initializer.rb.tt
|
|
@@ -193,12 +201,15 @@ files:
|
|
|
193
201
|
- lib/subflag/rails/client.rb
|
|
194
202
|
- lib/subflag/rails/configuration.rb
|
|
195
203
|
- lib/subflag/rails/context_builder.rb
|
|
204
|
+
- lib/subflag/rails/engine.rb
|
|
196
205
|
- lib/subflag/rails/evaluation_result.rb
|
|
197
206
|
- lib/subflag/rails/flag_accessor.rb
|
|
198
207
|
- lib/subflag/rails/helpers.rb
|
|
199
208
|
- lib/subflag/rails/models/flag.rb
|
|
200
209
|
- lib/subflag/rails/railtie.rb
|
|
201
210
|
- lib/subflag/rails/request_cache.rb
|
|
211
|
+
- lib/subflag/rails/targeting.rb
|
|
212
|
+
- lib/subflag/rails/targeting_engine.rb
|
|
202
213
|
- lib/subflag/rails/test_helpers.rb
|
|
203
214
|
- lib/subflag/rails/version.rb
|
|
204
215
|
homepage: https://subflag.com
|