mutations 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,149 @@
1
+ # IDEA i just had (protected parameters):
2
+ # optional do
3
+ # boolean :skip_confirmation, protected: true
4
+ # end
5
+ # Given the above, skip_confirmation is only accepted as a parameter if it's passed in a later hash, eg this would make it take:
6
+ # User::ChangeEmail.run!(params, user: current_user, skip_confirmation: true)
7
+ # But this would not:
8
+ # params = {user: current_user, skip_confirmation: true}
9
+ # User::ChangeEmail.run!(params)
10
+
11
+
12
+ module Mutations
13
+ class Command
14
+
15
+ ##
16
+ ##
17
+ ##
18
+ class << self
19
+ def required(&block)
20
+ self.input_filters.required(&block)
21
+
22
+ self.input_filters.required_keys.each do |key|
23
+ define_method(key) do
24
+ @filtered_input[key]
25
+ end
26
+
27
+ define_method("#{key}_present?") do
28
+ @filtered_input.has_key?(key)
29
+ end
30
+
31
+ define_method("#{key}=") do |v|
32
+ @filtered_input[key] = v
33
+ end
34
+ end
35
+ end
36
+
37
+ def optional(&block)
38
+ self.input_filters.optional(&block)
39
+
40
+ self.input_filters.optional_keys.each do |key|
41
+ define_method(key) do
42
+ @filtered_input[key]
43
+ end
44
+
45
+ define_method("#{key}_present?") do
46
+ @filtered_input.has_key?(key)
47
+ end
48
+
49
+ define_method("#{key}=") do |v|
50
+ @filtered_input[key] = v
51
+ end
52
+ end
53
+ end
54
+
55
+ def run(*args)
56
+ new(*args).execute!
57
+ end
58
+
59
+ def run!(*args)
60
+ m = run(*args)
61
+ if m.success?
62
+ m.result
63
+ else
64
+ raise ValidationException.new(m.errors)
65
+ end
66
+ end
67
+
68
+ def input_filters
69
+ @input_filters ||= begin
70
+ if Command == self.superclass
71
+ HashFilter.new
72
+ else
73
+ self.superclass.input_filters.dup
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ # Instance methods
81
+ def initialize(*args)
82
+ if args.length == 0
83
+ @original_hash = {}
84
+ else
85
+ @original_hash = args.shift
86
+ raise ArgumentError.new("All arguments must be hashes") unless @original_hash.is_a?(Hash)
87
+ @original_hash = @original_hash.with_indifferent_access
88
+ end
89
+
90
+ args.each do |a|
91
+ raise ArgumentError.new("All arguments must be hashes") unless a.is_a?(Hash)
92
+ @original_hash.merge!(a)
93
+ end
94
+ end
95
+
96
+ def input_filters
97
+ self.class.input_filters
98
+ end
99
+
100
+ def execute!
101
+ @filtered_input, @errors = self.input_filters.filter(@original_hash)
102
+ return Outcome.new(false, nil, @errors) if @errors
103
+
104
+ # IDEA/TODO: run validate block
105
+
106
+ r = execute
107
+ if @errors # Execute can add errors
108
+ return Outcome.new(false, nil, @errors)
109
+ else
110
+ return Outcome.new(true, r, nil)
111
+ end
112
+ end
113
+
114
+ # add_error("name", :too_short)
115
+ # add_error("colors.foreground", :not_a_color) # => to create errors = {colors: {foreground: :not_a_color}}
116
+ # or, supply a custom message:
117
+ # add_error("name", :too_short, "The name 'blahblahblah' is too short!")
118
+ def add_error(key, kind, message = nil)
119
+ raise ArgumentError.new("Invalid kind") unless kind.is_a?(Symbol)
120
+
121
+ @errors ||= ErrorHash.new
122
+ cur_errors = @errors
123
+ parts = key.to_s.split(".")
124
+ while part = parts.shift
125
+ part = part.to_sym
126
+ if parts.length > 0
127
+ cur_errors[part] = ErrorHash.new unless cur_errors[part].is_a?(ErrorHash)
128
+ cur_errors = cur_errors[part]
129
+ else
130
+ cur_errors[part] = ErrorAtom.new(key, kind, message: message)
131
+ end
132
+ end
133
+ @errors
134
+ end
135
+
136
+ def merge_errors(hash)
137
+ @errors ||= ErrorHash.new
138
+ @errors.merge!(hash)
139
+ end
140
+
141
+ def inputs
142
+ @filtered_input
143
+ end
144
+
145
+ def execute
146
+ # Meant to be overridden
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,152 @@
1
+ module Mutations
2
+
3
+ # Offers a non-localized, english only, non configurable way to get error messages. This probably isnt good enough for users as-is.
4
+ class DefaultErrorMessageCreator
5
+
6
+ MESSAGES = Hash.new("is invalid").tap do |h|
7
+ h.merge!(
8
+ # General
9
+ nils: "can't be nil",
10
+ required: "is required",
11
+
12
+ # Datatypes
13
+ string: "isn't a string",
14
+ integer: "isn't an integer",
15
+ boolean: "isn't a boolean",
16
+ hash: "isn't a hash",
17
+ array: "isn't an array",
18
+ model: "isn't the right class",
19
+
20
+ # String
21
+ empty: "can't be blank",
22
+ max_length: "is too long",
23
+ min_length: "is too short",
24
+ matches: "isn't in the right format",
25
+ in: "isn't an option",
26
+
27
+ # Array
28
+ class: "isn't the right class",
29
+
30
+ # Integer
31
+ min: "is too small",
32
+ max: "is too big",
33
+
34
+ # Model
35
+ new_records: "isn't a saved model"
36
+ )
37
+ end
38
+
39
+ # Key is either a symbol or a fixnum
40
+ def message(key, error_symbol, options = {})
41
+ if options[:index]
42
+ "#{(key || 'array').to_s.titleize}[#{options[:index]}] #{MESSAGES[error_symbol]}"
43
+ else
44
+ "#{key.to_s.titleize} #{MESSAGES[error_symbol]}"
45
+ end
46
+ end
47
+ end
48
+
49
+
50
+ class ErrorAtom
51
+
52
+ # NOTE: in the future, could also pass in:
53
+ # - error type
54
+ # - value (eg, string :name, length: 5 # value=5)
55
+
56
+ # ErrorAtom.new(:name, :too_short)
57
+ # ErrorAtom.new(:name, :too_short, message: "is too short")
58
+ def initialize(key, error_symbol, options = {})
59
+ @key = key
60
+ @symbol = error_symbol
61
+ @message = options[:message]
62
+ @index = options[:index]
63
+ end
64
+
65
+ def symbolic
66
+ @symbol
67
+ end
68
+
69
+ def message
70
+ @message ||= Mutations.error_message_creator.message(@key, @symbol, index: @index)
71
+ end
72
+
73
+ def message_list
74
+ Array(message)
75
+ end
76
+ end
77
+
78
+ # mutation.errors is an ErrorHash instance like this:
79
+ # {
80
+ # email: ErrorAtom(:matches),
81
+ # name: ErrorAtom(:too_weird, message: "is too weird"),
82
+ # adddress: { # Nested ErrorHash object
83
+ # city: ErrorAtom(:not_found, message: "That's not a city, silly!"),
84
+ # state: ErrorAtom(:in)
85
+ # }
86
+ # }
87
+ class ErrorHash < HashWithIndifferentAccess
88
+
89
+ # Returns a nested HashWithIndifferentAccess where the values are symbols. Eg:
90
+ # {
91
+ # email: :matches,
92
+ # name: :too_weird,
93
+ # adddress: {
94
+ # city: :not_found,
95
+ # state: :in
96
+ # }
97
+ # }
98
+ def symbolic
99
+ HashWithIndifferentAccess.new.tap do |hash|
100
+ each do |k, v|
101
+ hash[k] = v.symbolic
102
+ end
103
+ end
104
+ end
105
+
106
+ # Returns a nested HashWithIndifferentAccess where the values are messages. Eg:
107
+ # {
108
+ # email: "isn't in the right format",
109
+ # name: "is too weird",
110
+ # adddress: {
111
+ # city: "is not a city",
112
+ # state: "isn't a valid option"
113
+ # }
114
+ # }
115
+ def message
116
+ HashWithIndifferentAccess.new.tap do |hash|
117
+ each do |k, v|
118
+ hash[k] = v.message
119
+ end
120
+ end
121
+ end
122
+
123
+ # Returns a flat array where each element is a full sentence. Eg:
124
+ # [
125
+ # "Email isn't in the right format.",
126
+ # "Name is too weird",
127
+ # "That's not a city, silly!",
128
+ # "State isn't a valid option."
129
+ # ]
130
+ def message_list
131
+ list = []
132
+ each do |k, v|
133
+ list.concat(v.message_list)
134
+ end
135
+ list
136
+ end
137
+ end
138
+
139
+ class ErrorArray < Array
140
+ def symbolic
141
+ map {|e| e && e.symbolic }
142
+ end
143
+
144
+ def message
145
+ map {|e| e && e.message }
146
+ end
147
+
148
+ def message_list
149
+ compact.map {|e| e.message_list }.flatten
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,13 @@
1
+ module Mutations
2
+ class ValidationException < ::StandardError
3
+ attr_accessor :errors
4
+
5
+ def initialize(errors)
6
+ self.errors = errors
7
+ end
8
+
9
+ def to_s
10
+ "Mutations::ValidationException: #{self.errors.message_list.join('; ')}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,131 @@
1
+ module Mutations
2
+ class HashFilter < InputFilter
3
+ @default_options = {
4
+ nils: false, # true allows an explicit nil to be valid. Overrides any other options
5
+ }
6
+
7
+ attr_accessor :optional_inputs
8
+ attr_accessor :required_inputs
9
+
10
+ def initialize(opts = {}, &block)
11
+ super(opts)
12
+
13
+ @optional_inputs = {}
14
+ @required_inputs = {}
15
+ @current_inputs = @required_inputs
16
+
17
+ if block_given?
18
+ instance_eval &block
19
+ end
20
+ end
21
+
22
+ def dup
23
+ dupped = HashFilter.new
24
+ @optional_inputs.each_pair do |k, v|
25
+ dupped.optional_inputs[k] = v
26
+ end
27
+ @required_inputs.each_pair do |k, v|
28
+ dupped.required_inputs[k] = v
29
+ end
30
+ dupped
31
+ end
32
+
33
+ def required(&block)
34
+ # TODO: raise if nesting is wrong
35
+ @current_inputs = @required_inputs
36
+ instance_eval &block
37
+ end
38
+
39
+ def optional(&block)
40
+ # TODO: raise if nesting is wrong
41
+ @current_inputs = @optional_inputs
42
+ instance_eval &block
43
+ end
44
+
45
+ def required_keys
46
+ @required_inputs.keys
47
+ end
48
+
49
+ def optional_keys
50
+ @optional_inputs.keys
51
+ end
52
+
53
+ # Basic types:
54
+ def string(name, options = {})
55
+ @current_inputs[name.to_sym] = StringFilter.new(options)
56
+ end
57
+
58
+ def integer(name, options = {})
59
+ @current_inputs[name.to_sym] = IntegerFilter.new(options)
60
+ end
61
+
62
+ def boolean(name, options = {})
63
+ @current_inputs[name.to_sym] = BooleanFilter.new(options)
64
+ end
65
+
66
+ def hash(name, options = {}, &block)
67
+ @current_inputs[name.to_sym] = HashFilter.new(options, &block)
68
+ end
69
+
70
+ # Advanced types
71
+ def model(name, options = {})
72
+ name_sym = name.to_sym
73
+ @current_inputs[name_sym] = ModelFilter.new(name_sym, options)
74
+ end
75
+
76
+ def array(name, options = {}, &block)
77
+ name_sym = name.to_sym
78
+ @current_inputs[name.to_sym] = ArrayFilter.new(name_sym, options, &block)
79
+ end
80
+
81
+ def filter(data)
82
+
83
+ # Handle nil case
84
+ if data.nil?
85
+ return [nil, nil] if options[:nils]
86
+ return [nil, :nils]
87
+ end
88
+
89
+ # Ensure it's a hash
90
+ return [data, :hash] unless data.is_a?(Hash)
91
+
92
+ # We always want a hash with indiffernet access
93
+ unless data.is_a?(HashWithIndifferentAccess)
94
+ data = data.with_indifferent_access
95
+ end
96
+
97
+ errors = ErrorHash.new
98
+ filtered_data = HashWithIndifferentAccess.new
99
+
100
+ [[@required_inputs, true], [@optional_inputs, false]].each do |(inputs, is_required)|
101
+ inputs.each_pair do |key, filterer|
102
+ data_element = data[key]
103
+ default_used = false
104
+ if !data.has_key?(key) && filterer.has_default?
105
+ data_element = filterer.default
106
+ default_used = true
107
+ end
108
+
109
+ if data.has_key?(key) || default_used
110
+ sub_data, sub_error = filterer.filter(data_element)
111
+
112
+ if sub_error.nil?
113
+ filtered_data[key] = sub_data
114
+ else
115
+ sub_error = ErrorAtom.new(key, sub_error) if sub_error.is_a?(Symbol)
116
+ errors[key] = sub_error
117
+ end
118
+ elsif is_required
119
+ errors[key] = ErrorAtom.new(key, :required)
120
+ end
121
+ end
122
+ end
123
+
124
+ if errors.any?
125
+ [data, errors]
126
+ else
127
+ [filtered_data, nil] # We win, it's valid!
128
+ end
129
+ end
130
+ end
131
+ end