tram-policy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/setup"
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new :default
6
+
7
+ task default: :spec
data/bin/tram-policy ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path("lib/tram/policy/generator", Dir.pwd)
3
+
4
+ Tram::Policy::Generator.start
@@ -0,0 +1 @@
1
+ require_relative "tram/policy"
@@ -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