bodhi-slam 0.0.5 → 0.1.0

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.
@@ -0,0 +1,191 @@
1
+ module Bodhi
2
+ class Type
3
+ include Bodhi::Validations
4
+
5
+ BODHI_SYSTEM_ATTRIBUTES = [:sys_created_at, :sys_version, :sys_modified_at, :sys_modified_by,
6
+ :sys_namespace, :sys_created_by, :sys_type_version, :sys_id]
7
+ BODHI_TYPE_ATTRIBUTES = [:name, :namespace, :package, :embedded, :properties]
8
+
9
+ attr_accessor *BODHI_TYPE_ATTRIBUTES
10
+ attr_reader *BODHI_SYSTEM_ATTRIBUTES
11
+ attr_reader :validations
12
+
13
+ validates :name, required: true, is_not_blank: true
14
+ validates :namespace, required: true
15
+ validates :properties, required: true
16
+
17
+ def initialize(params={})
18
+ params.symbolize_keys!
19
+
20
+ BODHI_TYPE_ATTRIBUTES.each do |attribute|
21
+ send("#{attribute}=", params[attribute])
22
+ end
23
+
24
+ @validations = {}
25
+ if properties
26
+ properties.symbolize_keys!
27
+ properties.each_pair do |attr_name, attr_properties|
28
+ attr_properties.symbolize_keys!
29
+ @validations[attr_name] = []
30
+ attr_properties.each_pair do |option, value|
31
+ underscored_name = option.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase.to_sym
32
+ unless [:system, :trim, :ref, :unique, :default, :is_current_user].include? underscored_name
33
+ klass = Bodhi::Validator.constantize(underscored_name)
34
+ if option == :type && value == "Enumerated"
35
+ if attr_properties[:ref].nil?
36
+ raise RuntimeError.new("No reference property found! Cannot build enumeration validator for #{name}.#{attr_name}")
37
+ end
38
+
39
+ @validations[attr_name] << klass.new(value, attr_properties[:ref])
40
+ else
41
+ @validations[attr_name] << klass.new(value)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.find_all(context)
50
+ raise context.errors unless context.valid?
51
+
52
+ result = context.connection.get do |request|
53
+ request.url "/#{context.namespace}/types"
54
+ request.headers[context.credentials_header] = context.credentials
55
+ end
56
+
57
+ if result.status != 200
58
+ errors = JSON.parse result.body
59
+ errors.each{|error| error['status'] = result.status } if errors.is_a? Array
60
+ errors["status"] = result.status if errors.is_a? Hash
61
+ raise errors.to_s
62
+ end
63
+
64
+ JSON.parse(result.body).collect{ |type| Bodhi::Type.new(type) }
65
+ end
66
+
67
+ def self.create_class_with(type)
68
+ unless type.is_a? Bodhi::Type
69
+ raise ArgumentError.new("Expected #{type.class} to be a Bodhi::Type")
70
+ end
71
+
72
+ klass = Object.const_set(type.name, Class.new {
73
+ include Bodhi::Resource
74
+ attr_accessor *type.properties.keys
75
+ })
76
+
77
+ type.validations.each_pair do |attribute, validations|
78
+ attr_options = Hash.new
79
+ validations.each{ |validation| attr_options.merge!(validation.to_options) }
80
+ klass.validates(attribute, attr_options)
81
+ end
82
+
83
+ klass
84
+ end
85
+
86
+ def self.create_factory_with(type)
87
+ unless type.is_a? Bodhi::Type
88
+ raise ArgumentError.new("Expected #{type.class} to be a Bodhi::Type")
89
+ end
90
+
91
+ FactoryGirl.define do
92
+ factory type.name.to_sym do
93
+ type.properties.each_pair do |attribute, options|
94
+ unless options[:system]
95
+
96
+ case options[:type]
97
+ when "GeoJSON"
98
+ if options[:multi].nil?
99
+ send(attribute) { {type: "Point", coordinates: [10,20]} }
100
+ else
101
+ send(attribute) { [*0..5].sample.times.collect{ {type: "Point", coordinates: [10,20]} } }
102
+ end
103
+
104
+ when "Boolean"
105
+ if options[:multi].nil?
106
+ send(attribute) { [true, false].sample }
107
+ else
108
+ send(attribute) { [*0..5].sample.times.collect{ [true, false].sample } }
109
+ end
110
+
111
+ when "Enumerated"
112
+ reference = options[:ref].split('.')
113
+ name = reference[0]
114
+ field = reference[1]
115
+
116
+ if options[:multi].nil?
117
+ if field.nil?
118
+ send(attribute) { Bodhi::Enumeration.cache[name.to_sym].values.sample }
119
+ else
120
+ Bodhi::Enumeration.cache[name.to_sym].values.map!{ |value| value.symbolize_keys! }
121
+ send(attribute) { Bodhi::Enumeration.cache[name.to_sym].values.sample[field.to_sym] }
122
+ end
123
+ else
124
+ if field.nil?
125
+ send(attribute) { [*0..5].sample.times.collect{ Bodhi::Enumeration.cache[name.to_sym].values.sample } }
126
+ else
127
+ send(attribute) { [*0..5].sample.times.collect{ Bodhi::Enumeration.cache[name.to_sym].values.sample[field.to_sym] } }
128
+ end
129
+ end
130
+
131
+ when "Object"
132
+ if options[:multi].nil?
133
+ send(attribute) { {SecureRandom.hex => SecureRandom.hex} }
134
+ else
135
+ send(attribute) { [*0..5].sample.times.collect{ {SecureRandom.hex => SecureRandom.hex} } }
136
+ end
137
+
138
+ when "String"
139
+ if options[:multi].nil?
140
+ send(attribute) { SecureRandom.hex }
141
+ else
142
+ send(attribute) { [*0..5].sample.times.collect{ SecureRandom.hex } }
143
+ end
144
+
145
+ when "DateTime"
146
+ if options[:multi].nil?
147
+ send(attribute) { Time.at(rand * Time.now.to_i).iso8601 }
148
+ else
149
+ send(attribute) { [*0..5].sample.times.collect{ Time.at(rand * Time.now.to_i).iso8601 } }
150
+ end
151
+
152
+ when "Integer"
153
+ min = -10000
154
+ max = 10000
155
+ if options[:min]
156
+ min = options[:min]
157
+ end
158
+
159
+ if options[:max]
160
+ max = options[:max]
161
+ end
162
+
163
+ if options[:multi].nil?
164
+ send(attribute) { rand(min..max) }
165
+ else
166
+ send(attribute) { [*0..5].sample.times.collect{ rand(min..max) } }
167
+ end
168
+
169
+ when "Real"
170
+ if options[:multi].nil?
171
+ send(attribute) { SecureRandom.random_number*[-1,1,1,1].sample*[10,100,1000,10000].sample }
172
+ else
173
+ send(attribute) { [*0..5].sample.times.collect{ SecureRandom.random_number*[-1,1,1,1].sample*[10,100,1000,10000].sample } }
174
+ end
175
+
176
+ else # Its an embedded type
177
+ if options[:multi].nil?
178
+ send(attribute) { FactoryGirl.build(options[:type]) }
179
+ else
180
+ send(attribute) { [*0..5].sample.times.collect{ FactoryGirl.build(options[:type]) } }
181
+ end
182
+ end
183
+
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,141 @@
1
+ module Bodhi
2
+ module Validations
3
+
4
+ module ClassMethods
5
+
6
+ # Returns a Hash of all validations present for the class
7
+ #
8
+ # class User
9
+ # include Bodhi::Validations
10
+ # attr_accessor :name, :tags
11
+ #
12
+ # validates :tags, requried: true, multi: true
13
+ # validates :name, required: true
14
+ #
15
+ # User.validations
16
+ # # => {
17
+ # # tags: [
18
+ # # #<RequiredValidator:0x007fbff403e808 @options={}>,
19
+ # # #<MultiValidator:0x007fbff403e808 @options={}>
20
+ # # ],
21
+ # # name: [
22
+ # # #<RequiredValidator:0x007fbff403e808 @options={}>
23
+ # # ]
24
+ # # }
25
+ def validators; @validators; end
26
+
27
+ # Creates a new validation on the given +attribute+ using the supplied +options+
28
+ #
29
+ # class User
30
+ # include Bodhi::Validations
31
+ # attr_accessor :name, :address, :tags
32
+ #
33
+ # validates :name, type: "String", required: true
34
+ # validates :address, type: "PostalAddress", required: true
35
+ # validates :tags, type: "String", multi: true
36
+ def validates(attribute, options)
37
+ unless attribute.is_a? Symbol
38
+ raise ArgumentError.new("Invalid :attribute argument. Expected #{attribute.class} to be a Symbol")
39
+ end
40
+
41
+ unless options.is_a? Hash
42
+ raise ArgumentError.new("Invalid :options argument. Expected #{options.class} to be a Hash")
43
+ end
44
+
45
+ if options.keys.empty?
46
+ raise ArgumentError.new("Invalid :options argument. Options can not be empty")
47
+ end
48
+
49
+ @validators[attribute] = []
50
+ options.each_pair do |key, value|
51
+ unless [:ref].include?(key)
52
+ if key == :type && value == "Enumerated"
53
+ @validators[attribute] << Bodhi::Validator.constantize(key).new(value, options[:ref])
54
+ else
55
+ @validators[attribute] << Bodhi::Validator.constantize(key).new(value)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ module InstanceMethods
63
+
64
+ # Returns a +Bodhi::Errors+ object that holds all information about attribute error messages.
65
+ #
66
+ # class User
67
+ # include Bodhi::Validations
68
+ #
69
+ # attr_accessor :name
70
+ # validates :name, required: true
71
+ # end
72
+ #
73
+ # user = User.new
74
+ # user.valid? # => false
75
+ # user.errors # => #<Bodhi::Errors:0x007fbff403e808 @messages={name:["is required"]}>
76
+ def errors
77
+ @errors ||= Bodhi::Errors.new
78
+ end
79
+
80
+ # Runs all class validations on object and adds any errors to the +Bodhi::Errors+ object
81
+ #
82
+ # class User
83
+ # include Bodhi::Validations
84
+ #
85
+ # attr_accessor :name
86
+ # validates :name, required: true
87
+ # end
88
+ #
89
+ # user = User.new
90
+ # user.validate! # => nil
91
+ # user.errors.full_messages # => ["name is required"]
92
+ #
93
+ # user.name = "Bob"
94
+ # user.validate! # => nil
95
+ # user.errors.full_messages # => []
96
+ def validate!
97
+ errors.clear
98
+ self.class.validators.each_pair do |attribute, array|
99
+ value = self.send(attribute)
100
+ array.each do |validator|
101
+ validator.validate(self, attribute, value)
102
+ end
103
+ end
104
+ end
105
+
106
+ # Runs all validations and returns +true+ if no errors are present otherwise +false+.
107
+ #
108
+ # class User
109
+ # include Bodhi::Validations
110
+ #
111
+ # attr_accessor :name
112
+ # validates :name, required: true
113
+ # end
114
+ #
115
+ # user = User.new
116
+ # user.valid? # => false
117
+ # user.errors.full_messages # => ["name is required"]
118
+ #
119
+ # user.name = "Bob"
120
+ # user.valid? # => true
121
+ # user.errors.full_messages # => []
122
+ def valid?
123
+ validate!
124
+ !errors.messages.any?
125
+ end
126
+
127
+ # Runs all validations and returns +false+ if no errors are present otherwise +true+.
128
+ def invalid?
129
+ !valid?
130
+ end
131
+ end
132
+
133
+ def self.included(base)
134
+ base.extend(ClassMethods)
135
+ base.include(InstanceMethods)
136
+ base.instance_variable_set(:@validators, Hash.new)
137
+ end
138
+ end
139
+ end
140
+
141
+ require File.dirname(__FILE__) + "/validators.rb"
@@ -0,0 +1,60 @@
1
+ module Bodhi
2
+ class Validator
3
+
4
+ # Override this method in subclasses with validation logic, adding errors
5
+ # to the records +errors+ array where necessary.
6
+ def validate(record, attribute, value)
7
+ raise NotImplementedError, "Subclasses must implement a validate(record, attribute, value) method."
8
+ end
9
+
10
+ # Calls +underscore+ on the validation and returns it's class name as a symbol.
11
+ # Namespaces and the trailing "_validation" text will be trimmed
12
+ #
13
+ # BaseValidation.to_sym # => :base
14
+ # StringValidation.to_sym # => :string
15
+ # NotBlankValidation.to_sym # => :not_blank
16
+ def to_sym
17
+ underscore.
18
+ gsub("bodhi/", "").
19
+ gsub("_validator", "").
20
+ to_sym
21
+ end
22
+
23
+ # Returns the validation's class name in snake_case.
24
+ #
25
+ # BaseValidation.underscore # => "bodhi/base_validation"
26
+ # StringValidation.underscore # => "bodhi/string_validation"
27
+ # NotBlankValidation.underscore # => "bodhi/not_blank_validation"
28
+ def underscore
29
+ self.class.name.gsub(/::/, '/').
30
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
31
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
32
+ tr("-", "_").
33
+ downcase
34
+ end
35
+
36
+ # Returns the validation as an options Hash.
37
+ # The options hash is suitable to be used in the Bodhi::Validations.valdiates method
38
+ #
39
+ # StringValidation.to_options # => { string: true }
40
+ # EmbeddedValidation.to_options # => { embedded: "ClassName" }
41
+ # EnumeratedValidation.to_options # => { enumerated: "Country.name" }
42
+ def to_options
43
+ raise NotImplementedError, "Subclasses must implement a to_options method."
44
+ end
45
+
46
+ # Returns the validator class with the given +name+
47
+ # Raises NameError if no validator class is found
48
+ #
49
+ # Bodhi::Validator.constantize("type") # => Bodhi::TypeValidator
50
+ # Bodhi::Validator.constantize("multi") # => Bodhi::MutliValidator
51
+ # Bodhi::Validator.constantize("required") # => Bodhi::RequriedValidator
52
+ def self.constantize(name)
53
+ camelized_name = name.to_s.split('_').collect(&:capitalize).join
54
+ full_name = "Bodhi::#{camelized_name}Validator"
55
+ Object.const_get(full_name)
56
+ end
57
+ end
58
+ end
59
+
60
+ Dir[File.dirname(__FILE__) + "/validators/*.rb"].each { |file| require file }
@@ -0,0 +1,24 @@
1
+ module Bodhi
2
+ class IsNotBlankValidator < Validator
3
+
4
+ def initialize(value); end
5
+
6
+ def validate(record, attribute, value)
7
+ unless value.nil?
8
+
9
+ if value.is_a?(Array)
10
+ unless value.empty?
11
+ record.errors.add(attribute, "must not contain blank Strings") unless value.delete_if{ |v| !v.empty? }.empty?
12
+ end
13
+ else
14
+ record.errors.add(attribute, "can not be blank") if value.empty?
15
+ end
16
+
17
+ end
18
+ end
19
+
20
+ def to_options
21
+ {self.to_sym => true}
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Bodhi
2
+ class IsEmailValidator < Validator
3
+
4
+ def initialize(value); end
5
+
6
+ def validate(record, attribute, value)
7
+ unless value.nil?
8
+
9
+ if value.is_a?(Array)
10
+ unless value.empty?
11
+ record.errors.add(attribute, "must only contain valid email addresses") unless value.delete_if{ |v| v =~ /.+@.+\..+/i }.empty?
12
+ end
13
+ else
14
+ record.errors.add(attribute, "must be a valid email address") unless value =~ /.+@.+\..+/i
15
+ end
16
+
17
+ end
18
+ end
19
+
20
+ def to_options
21
+ {self.to_sym => true}
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module Bodhi
2
+ class MatchesValidator < Validator
3
+ attr_reader :value
4
+
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def validate(record, attribute, value)
10
+ unless value.nil?
11
+
12
+ if value.is_a?(Array)
13
+ unless value.empty?
14
+ record.errors.add(attribute, "must only contain values matching #{@value}") unless value.delete_if{ |v| v.match(@value) }.empty?
15
+ end
16
+ else
17
+ record.errors.add(attribute, "must match #{@value}") unless value.match(@value)
18
+ end
19
+
20
+ end
21
+ end
22
+
23
+ def to_options
24
+ {self.to_sym => @value}
25
+ end
26
+ end
27
+ end