tram-policy 0.0.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 +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +22 -0
- data/.travis.yml +27 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +373 -0
- data/Rakefile +7 -0
- data/bin/tram-policy +4 -0
- data/lib/tram-policy.rb +1 -0
- data/lib/tram/policy.rb +113 -0
- data/lib/tram/policy/error.rb +88 -0
- data/lib/tram/policy/errors.rb +102 -0
- data/lib/tram/policy/generator.rb +111 -0
- data/lib/tram/policy/generator/policy.erb +20 -0
- data/lib/tram/policy/generator/policy_spec.erb +17 -0
- data/lib/tram/policy/inflector.rb +26 -0
- data/lib/tram/policy/matchers.rb +112 -0
- data/lib/tram/policy/validation_error.rb +18 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/tram/policy/error_spec.rb +61 -0
- data/spec/tram/policy/errors_spec.rb +112 -0
- data/spec/tram/policy/inflector_spec.rb +14 -0
- data/spec/tram/policy/matchers_spec.rb +70 -0
- data/spec/tram/policy/validation_error_spec.rb +23 -0
- data/spec/tram/policy_spec.rb +173 -0
- data/tram-policy.gemspec +25 -0
- metadata +182 -0
data/Rakefile
ADDED
data/bin/tram-policy
ADDED
data/lib/tram-policy.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "tram/policy"
|
data/lib/tram/policy.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require "dry-initializer"
|
2
|
+
require "i18n"
|
3
|
+
|
4
|
+
module Tram
|
5
|
+
# Base class for policy objects with composable validation errors
|
6
|
+
class Policy
|
7
|
+
require_relative "policy/validation_error"
|
8
|
+
require_relative "policy/inflector"
|
9
|
+
require_relative "policy/error"
|
10
|
+
require_relative "policy/errors"
|
11
|
+
|
12
|
+
extend Dry::Initializer
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# Registers a validator
|
16
|
+
#
|
17
|
+
# @param [#to_sym, Array<#to_sym>] names
|
18
|
+
# @return [self]
|
19
|
+
#
|
20
|
+
def validate(*names)
|
21
|
+
@validators = validators | names.flatten.map(&:to_sym)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Policy constructor/validator (alias for [.new])
|
26
|
+
#
|
27
|
+
# @param [Object] *args
|
28
|
+
# @return [Tram::Policy]
|
29
|
+
#
|
30
|
+
def [](*args)
|
31
|
+
new(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validators
|
37
|
+
@validators ||= []
|
38
|
+
end
|
39
|
+
|
40
|
+
def inherited(klass)
|
41
|
+
super
|
42
|
+
klass.validate validators
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Translates a message in the scope of current policy
|
47
|
+
#
|
48
|
+
# @param [#to_s] message
|
49
|
+
# @param [Hash<Symbol, Object>] options
|
50
|
+
# @return [String]
|
51
|
+
#
|
52
|
+
def t(message, **options)
|
53
|
+
return message.to_s unless message.is_a? Symbol
|
54
|
+
I18n.t message, options.merge(scope: @__scope__)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Collection of validation errors
|
58
|
+
#
|
59
|
+
# @return [Tram::Policy::Errors]
|
60
|
+
#
|
61
|
+
def errors
|
62
|
+
@errors ||= Errors.new(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Checks whether the policy is valid
|
66
|
+
#
|
67
|
+
# @param [Proc, nil] filter Block describing **errors to be skipped**
|
68
|
+
# @return [Boolean]
|
69
|
+
#
|
70
|
+
def valid?(&filter)
|
71
|
+
filter ? errors.reject(&filter).empty? : errors.empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
# Checks whether the policy is invalid
|
75
|
+
#
|
76
|
+
# @param [Proc, nil] filter Block describing **the only errors to count**
|
77
|
+
# @return [Boolean]
|
78
|
+
#
|
79
|
+
def invalid?(&filter)
|
80
|
+
filter ? errors.any?(&filter) : errors.any?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Raises exception if the policy is not valid
|
84
|
+
#
|
85
|
+
# @param (see #valid?)
|
86
|
+
# @raise [Tram::Policy::ValidationError] if the policy isn't valid
|
87
|
+
# @return [nil] if the policy is valid
|
88
|
+
#
|
89
|
+
def validate!(&filter)
|
90
|
+
raise ValidationError.new(self, filter) unless valid?(&filter)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Human-readable representation of the policy
|
94
|
+
#
|
95
|
+
# @example Displays policy name and its attributes
|
96
|
+
# UserPolicy[name: "Andy"].inspect
|
97
|
+
# # => #<UserPolicy["name" => "Andy"]>
|
98
|
+
#
|
99
|
+
# @return [String]
|
100
|
+
#
|
101
|
+
def inspect
|
102
|
+
"#<#{self.class.name}[#{@__options__}]>"
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def initialize(*)
|
108
|
+
super
|
109
|
+
@__scope__ = Inflector.underscore(self.class.name)
|
110
|
+
self.class.send(:validators).each { |name| send(name) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class Tram::Policy
|
2
|
+
# Validation error with message and assigned tags
|
3
|
+
#
|
4
|
+
# Notice: an error is context-independent; it knows nothing about
|
5
|
+
# a collection it is placed to; it can be safely moved
|
6
|
+
# from one collection of [Tram::Policy::Errors] to another.
|
7
|
+
#
|
8
|
+
class Error
|
9
|
+
# Builds an error
|
10
|
+
#
|
11
|
+
# If another error is send to the constructor, the error returned unchanged
|
12
|
+
#
|
13
|
+
# @param [Tram::Policy::Error, #to_s] value
|
14
|
+
# @param [Hash<Symbol, Object>] opts
|
15
|
+
# @return [Tram::Policy::Error]
|
16
|
+
#
|
17
|
+
def self.new(value, **opts)
|
18
|
+
return value if value.is_a? self
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!attribute [r] message
|
23
|
+
#
|
24
|
+
# @return [String] The error message text
|
25
|
+
#
|
26
|
+
attr_reader :message
|
27
|
+
|
28
|
+
# The full message (message and tags info)
|
29
|
+
#
|
30
|
+
# @return [String]
|
31
|
+
#
|
32
|
+
def full_message
|
33
|
+
[message, @tags].reject(&:empty?).join(" ")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Converts the error to a simple hash with message and tags
|
37
|
+
#
|
38
|
+
# @return [Hash<Symbol, Object>]
|
39
|
+
#
|
40
|
+
def to_h
|
41
|
+
@tags.merge(message: message)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Fetches either message or a tag
|
45
|
+
#
|
46
|
+
# @param [#to_sym] tag
|
47
|
+
# @return [Object]
|
48
|
+
#
|
49
|
+
def [](tag)
|
50
|
+
to_h[tag.to_sym]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Fetches either message or a tag
|
54
|
+
#
|
55
|
+
# @param [#to_sym] tag
|
56
|
+
# @param [Object] default
|
57
|
+
# @param [Proc] block
|
58
|
+
# @return [Object]
|
59
|
+
#
|
60
|
+
def fetch(tag, default, &block)
|
61
|
+
to_h.fetch(tag.to_sym, default, &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Compares an error to another object using method [#to_h]
|
65
|
+
#
|
66
|
+
# @param [Object] other Other object to compare to
|
67
|
+
# @return [Boolean]
|
68
|
+
#
|
69
|
+
def ==(other)
|
70
|
+
other.respond_to?(:to_h) && other.to_h == to_h
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def initialize(message, **tags)
|
76
|
+
@message = message.to_s
|
77
|
+
@tags = tags
|
78
|
+
end
|
79
|
+
|
80
|
+
def respond_to_missing?(*)
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def method_missing(name, *args, &block)
|
85
|
+
args.any? || block ? super : @tags[name]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
class Tram::Policy
|
2
|
+
# Enumerable collection of unique unordered validation errors
|
3
|
+
#
|
4
|
+
# Notice: A collection is context-dependent;
|
5
|
+
# it knows about a scope of policy it belongs to,
|
6
|
+
# and how to translate error messages in that scope.
|
7
|
+
#
|
8
|
+
class Errors
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
# @!attribute [r] policy
|
12
|
+
#
|
13
|
+
# @return [Tram::Policy] the poplicy errors provided by
|
14
|
+
#
|
15
|
+
attr_reader :policy
|
16
|
+
|
17
|
+
# Adds error message to the collection
|
18
|
+
#
|
19
|
+
# @param [#to_s] message Either a message, or a symbolic key for translation
|
20
|
+
# @param [Hash<Symbol, Object>] tags Tags to be attached to the message
|
21
|
+
# @return [self] the collection
|
22
|
+
#
|
23
|
+
def add(message = nil, **tags)
|
24
|
+
message ||= tags.delete(:message)
|
25
|
+
raise ArgumentError.new("Error message should be defined") unless message
|
26
|
+
|
27
|
+
@set << Tram::Policy::Error.new(@policy.t(message, tags), **tags)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Iterates by collected errors
|
32
|
+
#
|
33
|
+
# @yeldparam [Tram::Policy::Error]
|
34
|
+
# @return [Enumerator<Tram::Policy::Error>]
|
35
|
+
#
|
36
|
+
def each
|
37
|
+
@set.each { |error| yield(error) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Selects errors filtered by tags
|
41
|
+
#
|
42
|
+
# @param [Hash<Symbol, Object>] filter
|
43
|
+
# @return [Hash<Symbol, Object>]
|
44
|
+
#
|
45
|
+
def by_tags(**filter)
|
46
|
+
filter = filter.to_a
|
47
|
+
reject { |error| (filter - error.to_h.to_a).any? }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Checks whether a collection is empty
|
51
|
+
#
|
52
|
+
# @return [Boolean]
|
53
|
+
#
|
54
|
+
def empty?
|
55
|
+
!any?
|
56
|
+
end
|
57
|
+
|
58
|
+
# The array of ordered error messages
|
59
|
+
#
|
60
|
+
# @return [Array<String>]
|
61
|
+
#
|
62
|
+
def messages
|
63
|
+
@set.map(&:message).sort
|
64
|
+
end
|
65
|
+
|
66
|
+
# The array of ordered error messages with error tags info
|
67
|
+
#
|
68
|
+
# @return [Array<String>]
|
69
|
+
#
|
70
|
+
def full_messages
|
71
|
+
@set.map(&:full_message).sort
|
72
|
+
end
|
73
|
+
|
74
|
+
# Merges other collection to the current one and returns new collection
|
75
|
+
# with the current scope
|
76
|
+
#
|
77
|
+
# param [Tram::Policy::Errors] other Collection to be merged
|
78
|
+
# yieldparam [Hash<Symbol, Object>]
|
79
|
+
#
|
80
|
+
# @example Add some tag to merged errors
|
81
|
+
# policy.merge(other) { |err| err[:source] = "other" }
|
82
|
+
#
|
83
|
+
def merge(other)
|
84
|
+
return self unless other.is_a?(self.class) && other.any?
|
85
|
+
|
86
|
+
if block_given?
|
87
|
+
other.each { |err| add yield(err.to_h) }
|
88
|
+
else
|
89
|
+
@set |= other.to_a
|
90
|
+
end
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def initialize(policy, errors = Set.new)
|
98
|
+
@policy = policy
|
99
|
+
@set = errors
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require "thor/group"
|
2
|
+
require "i18n"
|
3
|
+
|
4
|
+
module Tram
|
5
|
+
class Policy
|
6
|
+
require_relative "inflector"
|
7
|
+
|
8
|
+
class Generator < Thor::Group
|
9
|
+
include Thor::Actions
|
10
|
+
|
11
|
+
desc "Generates new policy class along with its specification"
|
12
|
+
argument :name, desc: "policy class name", type: :string
|
13
|
+
class_option :params, desc: "list of policy params",
|
14
|
+
type: :array,
|
15
|
+
default: [],
|
16
|
+
aliases: "-p",
|
17
|
+
banner: "param[ param]"
|
18
|
+
class_option :options, desc: "list of policy options",
|
19
|
+
type: :array,
|
20
|
+
default: [],
|
21
|
+
aliases: "-o",
|
22
|
+
banner: "option[ option]"
|
23
|
+
class_option :validators, desc: "list of policy validators",
|
24
|
+
type: :array,
|
25
|
+
default: [],
|
26
|
+
aliases: "-v",
|
27
|
+
banner: "validator[ validator]"
|
28
|
+
|
29
|
+
def self.source_root
|
30
|
+
File.dirname(__FILE__)
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_class
|
34
|
+
template "generator/policy.erb", "app/policies/#{file}.rb"
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_locales
|
38
|
+
available_locales.each do |locale|
|
39
|
+
@locale = locale
|
40
|
+
add_locale
|
41
|
+
localize_policy
|
42
|
+
parsed_validators.each { |validator| localize_validator(validator) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_spec
|
47
|
+
template "generator/policy_spec.erb", "spec/policies/#{file}_spec.rb"
|
48
|
+
end
|
49
|
+
|
50
|
+
no_tasks do
|
51
|
+
def klass
|
52
|
+
@klass ||= Inflector.camelize name
|
53
|
+
end
|
54
|
+
|
55
|
+
def file
|
56
|
+
@file ||= Inflector.underscore name
|
57
|
+
end
|
58
|
+
|
59
|
+
def parsed_options
|
60
|
+
@parsed_options ||= options[:options].map(&:downcase)
|
61
|
+
end
|
62
|
+
|
63
|
+
def parsed_params
|
64
|
+
@parsed_params ||= options[:params].map(&:downcase)
|
65
|
+
end
|
66
|
+
|
67
|
+
def parsed_validators
|
68
|
+
@parsed_validators ||= options[:validators].map(&:downcase)
|
69
|
+
end
|
70
|
+
|
71
|
+
def policy_signature
|
72
|
+
@policy_signature ||= \
|
73
|
+
parsed_params + \
|
74
|
+
parsed_options.map { |option| "#{option}: #{option}" }
|
75
|
+
end
|
76
|
+
|
77
|
+
def available_locales
|
78
|
+
ask("What locales should be used for translation?").scan(/\w{2}/)
|
79
|
+
end
|
80
|
+
|
81
|
+
def locale_file
|
82
|
+
"config/locales/tram-policy.#{@locale}.yml"
|
83
|
+
end
|
84
|
+
|
85
|
+
def locale_header
|
86
|
+
"---\n#{@locale}:\n"
|
87
|
+
end
|
88
|
+
|
89
|
+
def locale_group
|
90
|
+
@locale_group ||= " #{file}:\n"
|
91
|
+
end
|
92
|
+
|
93
|
+
def locale_line(validator)
|
94
|
+
" #{validator}: #{validator}\n"
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_locale
|
98
|
+
create_file(locale_file, skip: true) { locale_header }
|
99
|
+
end
|
100
|
+
|
101
|
+
def localize_policy
|
102
|
+
append_to_file(locale_file, locale_group)
|
103
|
+
end
|
104
|
+
|
105
|
+
def localize_validator(name)
|
106
|
+
insert_into_file locale_file, locale_line(name), after: locale_group
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class <%= klass %> < Tram::Policy
|
2
|
+
<% parsed_params.each do |param| -%>
|
3
|
+
param :<%= param %>
|
4
|
+
<% end -%>
|
5
|
+
<% parsed_options.each do |option| -%>
|
6
|
+
option :<%= option %>
|
7
|
+
<% end -%>
|
8
|
+
|
9
|
+
<% parsed_validators.each do |validator| -%>
|
10
|
+
validate :<%= validator %>
|
11
|
+
<% end -%>
|
12
|
+
|
13
|
+
private
|
14
|
+
<% parsed_validators.each do |validator| %>
|
15
|
+
def <%= validator %>
|
16
|
+
return if true # define a condition
|
17
|
+
errors.add :<%= validator %> # add necessary tags
|
18
|
+
end
|
19
|
+
<% end -%>
|
20
|
+
end
|