tram-policy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|