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.
- checksums.yaml +4 -4
- data/lib/bodhi-slam.rb +13 -109
- data/lib/bodhi-slam/context.rb +37 -51
- data/lib/bodhi-slam/enumerations.rb +35 -0
- data/lib/bodhi-slam/errors.rb +81 -4
- data/lib/bodhi-slam/resource.rb +201 -64
- data/lib/bodhi-slam/types.rb +191 -0
- data/lib/bodhi-slam/validations.rb +141 -0
- data/lib/bodhi-slam/validators.rb +60 -0
- data/lib/bodhi-slam/validators/blank.rb +24 -0
- data/lib/bodhi-slam/validators/email.rb +24 -0
- data/lib/bodhi-slam/validators/matches.rb +27 -0
- data/lib/bodhi-slam/validators/max.rb +27 -0
- data/lib/bodhi-slam/validators/min.rb +27 -0
- data/lib/bodhi-slam/validators/multi.rb +14 -0
- data/lib/bodhi-slam/validators/not_empty.rb +20 -0
- data/lib/bodhi-slam/validators/required.rb +14 -0
- data/lib/bodhi-slam/validators/type.rb +123 -0
- data/lib/bodhi-slam/validators/url.rb +24 -0
- metadata +61 -5
@@ -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
|