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.
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