vcap_common 1.0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/json_message.rb +139 -0
- data/lib/json_schema.rb +84 -0
- data/lib/services/api/async_requests.rb +43 -0
- data/lib/services/api/clients/service_gateway_client.rb +107 -0
- data/lib/services/api/const.rb +9 -0
- data/lib/services/api/messages.rb +153 -0
- data/lib/services/api/util.rb +17 -0
- data/lib/services/api.rb +6 -0
- data/lib/vcap/common.rb +236 -0
- data/lib/vcap/component.rb +172 -0
- data/lib/vcap/config.rb +32 -0
- data/lib/vcap/fiber_tracing.rb +45 -0
- data/lib/vcap/json_schema.rb +202 -0
- data/lib/vcap/priority_queue.rb +164 -0
- data/lib/vcap/process_utils.rb +43 -0
- data/lib/vcap/quota.rb +152 -0
- data/lib/vcap/rolling_metric.rb +74 -0
- data/lib/vcap/spec/em.rb +32 -0
- data/lib/vcap/spec/forked_component/base.rb +87 -0
- data/lib/vcap/spec/forked_component/nats_server.rb +28 -0
- data/lib/vcap/spec/forked_component.rb +2 -0
- data/lib/vcap/subprocess.rb +211 -0
- data/lib/vcap/user_pools/user_ops.rb +47 -0
- data/lib/vcap/user_pools/user_pool.rb +45 -0
- data/lib/vcap/user_pools/user_pool_util.rb +107 -0
- metadata +166 -0
data/lib/vcap/common.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
require 'fileutils'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
# VMware's Cloud Application Platform
|
6
|
+
|
7
|
+
module VCAP
|
8
|
+
|
9
|
+
A_ROOT_SERVER = '198.41.0.4'
|
10
|
+
|
11
|
+
def self.local_ip(route = A_ROOT_SERVER)
|
12
|
+
route ||= A_ROOT_SERVER
|
13
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
|
14
|
+
UDPSocket.open {|s| s.connect(route, 1); s.addr.last }
|
15
|
+
ensure
|
16
|
+
Socket.do_not_reverse_lookup = orig
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.secure_uuid
|
20
|
+
result = File.open('/dev/urandom') { |x| x.read(16).unpack('H*')[0] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.grab_ephemeral_port
|
24
|
+
socket = TCPServer.new('0.0.0.0', 0)
|
25
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
26
|
+
Socket.do_not_reverse_lookup = true
|
27
|
+
port = socket.addr[1]
|
28
|
+
socket.close
|
29
|
+
return port
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.uptime_string(delta)
|
33
|
+
num_seconds = delta.to_i
|
34
|
+
days = num_seconds / (60 * 60 * 24);
|
35
|
+
num_seconds -= days * (60 * 60 * 24);
|
36
|
+
hours = num_seconds / (60 * 60);
|
37
|
+
num_seconds -= hours * (60 * 60);
|
38
|
+
minutes = num_seconds / 60;
|
39
|
+
num_seconds -= minutes * 60;
|
40
|
+
"#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s"
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.num_cores
|
44
|
+
if RUBY_PLATFORM =~ /linux/
|
45
|
+
return `cat /proc/cpuinfo | grep processor | wc -l`.to_i
|
46
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
47
|
+
`hwprefs cpu_count`.strip.to_i
|
48
|
+
elsif RUBY_PLATFORM =~ /freebsd|netbsd/
|
49
|
+
`sysctl hw.ncpu`.strip.to_i
|
50
|
+
else
|
51
|
+
return 1 # unknown..
|
52
|
+
end
|
53
|
+
rescue
|
54
|
+
# hwprefs doesn't always exist, and so the block above can fail.
|
55
|
+
# In any case, let's always assume that there is 1 core
|
56
|
+
1
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.defer(*args, &blk)
|
60
|
+
if args[0].kind_of?(Hash)
|
61
|
+
op = blk
|
62
|
+
opts = args[0]
|
63
|
+
else
|
64
|
+
op = args[0] || blk
|
65
|
+
opts = args[1] || {}
|
66
|
+
end
|
67
|
+
|
68
|
+
callback = opts[:callback]
|
69
|
+
logger = opts[:logger]
|
70
|
+
nobacktrace = opts[:nobacktrace]
|
71
|
+
|
72
|
+
wrapped_operation = exception_wrap_block(op, logger, nobacktrace)
|
73
|
+
wrapped_callback = callback ? exception_wrap_block(callback, logger, nobacktrace) : nil
|
74
|
+
EM.defer(wrapped_operation, wrapped_callback)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.exception_wrap_block(op, logger, nobacktrace=false)
|
78
|
+
Proc.new do |*args|
|
79
|
+
begin
|
80
|
+
op.call(*args)
|
81
|
+
rescue => e
|
82
|
+
err_str = "#{e} - #{e.backtrace.join("\n")}" unless nobacktrace
|
83
|
+
err_str = "#{e}" if nobacktrace
|
84
|
+
if logger
|
85
|
+
logger.fatal(err_str)
|
86
|
+
else
|
87
|
+
$stderr.puts(err_str)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.process_running?(pid)
|
94
|
+
return false unless pid && (pid > 0)
|
95
|
+
output = %x[ps -o rss= -p #{pid}]
|
96
|
+
return true if ($? == 0 && !output.empty?)
|
97
|
+
# fail otherwise..
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.pp_bytesize(bsize)
|
102
|
+
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
103
|
+
base = 1000
|
104
|
+
bsize = bsize.to_f()
|
105
|
+
quotient = unit = nil
|
106
|
+
units.each_with_index do |u, i|
|
107
|
+
unit = u
|
108
|
+
quotient = bsize / (base ** i)
|
109
|
+
break if quotient < base
|
110
|
+
end
|
111
|
+
"%0.2f%s" % [quotient, unit]
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.symbolize_keys(hash)
|
115
|
+
if hash.is_a? Hash
|
116
|
+
new_hash = {}
|
117
|
+
hash.each {|k, v| new_hash[k.to_sym] = symbolize_keys(v) }
|
118
|
+
new_hash
|
119
|
+
else
|
120
|
+
hash
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Helper class to atomically create/update pidfiles and ensure that only one instance of a given
|
125
|
+
# process is running at all times.
|
126
|
+
#
|
127
|
+
# NB: Ruby doesn't have real destructors so if you want to be polite and clean up after yourself
|
128
|
+
# be sure to call unlink() before your process exits.
|
129
|
+
#
|
130
|
+
# usage:
|
131
|
+
#
|
132
|
+
# begin
|
133
|
+
# pidfile = VCAP::PidFile.new('/tmp/foo')
|
134
|
+
# rescue => e
|
135
|
+
# puts "Error creating pidfile: %s" % (e)
|
136
|
+
# exit(1)
|
137
|
+
# end
|
138
|
+
class PidFile
|
139
|
+
class ProcessRunningError < StandardError
|
140
|
+
end
|
141
|
+
|
142
|
+
def initialize(pid_file, create_parents=true)
|
143
|
+
@pid_file = pid_file
|
144
|
+
@dirty = true
|
145
|
+
write(create_parents)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Removes the created pidfile
|
149
|
+
def unlink()
|
150
|
+
return unless @dirty
|
151
|
+
|
152
|
+
# Swallowing exception here is fine. Removing the pid files is a courtesy.
|
153
|
+
begin
|
154
|
+
File.unlink(@pid_file)
|
155
|
+
@dirty = false
|
156
|
+
rescue
|
157
|
+
end
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
# Removes the created pidfile upon receipt of the supplied signals
|
162
|
+
def unlink_on_signals(*sigs)
|
163
|
+
return unless @dirty
|
164
|
+
|
165
|
+
sigs.each do |s|
|
166
|
+
Signal.trap(s) { unlink() }
|
167
|
+
end
|
168
|
+
self
|
169
|
+
end
|
170
|
+
|
171
|
+
def unlink_at_exit()
|
172
|
+
at_exit { unlink() }
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_s()
|
177
|
+
@pid_file
|
178
|
+
end
|
179
|
+
|
180
|
+
protected
|
181
|
+
|
182
|
+
# Atomically writes the pidfile.
|
183
|
+
# NB: This throws exceptions if the pidfile contains the pid of another running process.
|
184
|
+
#
|
185
|
+
# +create_parents+ If true, all parts of the path up to the file's dirname will be created.
|
186
|
+
#
|
187
|
+
def write(create_parents=true)
|
188
|
+
FileUtils.mkdir_p(File.dirname(@pid_file)) if create_parents
|
189
|
+
|
190
|
+
# Protip from Wilson: binary mode keeps things sane under Windows
|
191
|
+
# Closing the fd releases our lock
|
192
|
+
File.open(@pid_file, 'a+b', 0644) do |f|
|
193
|
+
f.flock(File::LOCK_EX)
|
194
|
+
|
195
|
+
# Check if process is already running
|
196
|
+
pid = f.read().strip().to_i()
|
197
|
+
if pid == Process.pid()
|
198
|
+
return
|
199
|
+
elsif VCAP.process_running?(pid)
|
200
|
+
raise ProcessRunningError.new("Process already running (pid=%d)." % (pid))
|
201
|
+
end
|
202
|
+
|
203
|
+
# We're good to go, write our pid
|
204
|
+
f.truncate(0)
|
205
|
+
f.rewind()
|
206
|
+
f.write("%d\n" % (Process.pid()))
|
207
|
+
f.flush()
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end # class PidFile
|
211
|
+
|
212
|
+
end # module VCAP
|
213
|
+
|
214
|
+
# Make the patch here for proper bytesize
|
215
|
+
if RUBY_VERSION <= "1.8.6"
|
216
|
+
class String #:nodoc:
|
217
|
+
def bytesize; self.size; end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# FIXME, we should ditch ruby logger.
|
222
|
+
# Monkey Patch to get rid of some deadlocks under load in CC, make it available for all.
|
223
|
+
|
224
|
+
require 'logger'
|
225
|
+
|
226
|
+
STDOUT.sync = true
|
227
|
+
|
228
|
+
class Logger::LogDevice
|
229
|
+
def write(message)
|
230
|
+
@dev.syswrite(message)
|
231
|
+
end
|
232
|
+
|
233
|
+
def close
|
234
|
+
@dev.close
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
require "eventmachine"
|
3
|
+
require 'thin'
|
4
|
+
require "yajl"
|
5
|
+
require "nats/client"
|
6
|
+
require "base64"
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
module VCAP
|
10
|
+
|
11
|
+
RACK_JSON_HDR = { 'Content-Type' => 'application/json' }
|
12
|
+
RACK_TEXT_HDR = { 'Content-Type' => 'text/plaintext' }
|
13
|
+
|
14
|
+
class Varz
|
15
|
+
def initialize(logger)
|
16
|
+
@logger = logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
@logger.debug "varz access"
|
21
|
+
varz = Yajl::Encoder.encode(Component.updated_varz, :pretty => true, :terminator => "\n")
|
22
|
+
[200, { 'Content-Type' => 'application/json', 'Content-Length' => varz.length.to_s }, varz]
|
23
|
+
rescue => e
|
24
|
+
@logger.error "varz error #{e.inspect} #{e.backtrace.join("\n")}"
|
25
|
+
raise e
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Healthz
|
30
|
+
def initialize(logger)
|
31
|
+
@logger = logger
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(env)
|
35
|
+
@logger.debug "healthz access"
|
36
|
+
healthz = Component.updated_healthz
|
37
|
+
[200, { 'Content-Type' => 'application/json', 'Content-Length' => healthz.length.to_s }, healthz]
|
38
|
+
rescue => e
|
39
|
+
@logger.error "healthz error #{e.inspect} #{e.backtrace.join("\n")}"
|
40
|
+
raise e
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Common component setup for discovery and monitoring
|
45
|
+
class Component
|
46
|
+
|
47
|
+
# We will suppress these from normal varz reporting by default.
|
48
|
+
CONFIG_SUPPRESS = Set.new([:mbus, :service_mbus, :keys, :database_environment, :mysql, :password])
|
49
|
+
|
50
|
+
class << self
|
51
|
+
|
52
|
+
attr_reader :varz
|
53
|
+
attr_accessor :healthz
|
54
|
+
|
55
|
+
def updated_varz
|
56
|
+
@last_varz_update ||= 0
|
57
|
+
if Time.now.to_f - @last_varz_update >= 1
|
58
|
+
# Snapshot uptime
|
59
|
+
@varz[:uptime] = VCAP.uptime_string(Time.now - @varz[:start])
|
60
|
+
|
61
|
+
# Grab current cpu and memory usage.
|
62
|
+
rss, pcpu = `ps -o rss=,pcpu= -p #{Process.pid}`.split
|
63
|
+
@varz[:mem] = rss.to_i
|
64
|
+
@varz[:cpu] = pcpu.to_f
|
65
|
+
|
66
|
+
@last_varz_update = Time.now.to_f
|
67
|
+
end
|
68
|
+
varz
|
69
|
+
end
|
70
|
+
|
71
|
+
def updated_healthz
|
72
|
+
@last_healthz_update ||= 0
|
73
|
+
if Time.now.to_f - @last_healthz_update >= 1
|
74
|
+
# ...
|
75
|
+
@last_healthz_update = Time.now.to_f
|
76
|
+
end
|
77
|
+
|
78
|
+
healthz
|
79
|
+
end
|
80
|
+
|
81
|
+
def start_http_server(host, port, auth, logger)
|
82
|
+
http_server = Thin::Server.new(host, port, :signals => false) do
|
83
|
+
Thin::Logging.silent = true
|
84
|
+
use Rack::Auth::Basic do |username, password|
|
85
|
+
[username, password] == auth
|
86
|
+
end
|
87
|
+
map '/healthz' do
|
88
|
+
run Healthz.new(logger)
|
89
|
+
end
|
90
|
+
map '/varz' do
|
91
|
+
run Varz.new(logger)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
http_server.start!
|
95
|
+
end
|
96
|
+
|
97
|
+
def uuid
|
98
|
+
@discover[:uuid]
|
99
|
+
end
|
100
|
+
|
101
|
+
def register(opts)
|
102
|
+
uuid = VCAP.secure_uuid
|
103
|
+
type = opts[:type]
|
104
|
+
index = opts[:index]
|
105
|
+
uuid = "#{index}-#{uuid}" if index
|
106
|
+
host = opts[:host] || VCAP.local_ip
|
107
|
+
port = opts[:port] || VCAP.grab_ephemeral_port
|
108
|
+
nats = opts[:nats] || NATS
|
109
|
+
auth = [opts[:user] || VCAP.secure_uuid, opts[:password] || VCAP.secure_uuid]
|
110
|
+
logger = opts[:logger] || Logger.new(nil)
|
111
|
+
|
112
|
+
# Discover message limited
|
113
|
+
@discover = {
|
114
|
+
:type => type,
|
115
|
+
:index => index,
|
116
|
+
:uuid => uuid,
|
117
|
+
:host => "#{host}:#{port}",
|
118
|
+
:credentials => auth,
|
119
|
+
:start => Time.now
|
120
|
+
}
|
121
|
+
|
122
|
+
# Varz is customizable
|
123
|
+
@varz = @discover.dup
|
124
|
+
@varz[:num_cores] = VCAP.num_cores
|
125
|
+
@varz[:config] = sanitize_config(opts[:config]) if opts[:config]
|
126
|
+
|
127
|
+
@healthz = "ok\n".freeze
|
128
|
+
|
129
|
+
# Next steps require EM
|
130
|
+
raise "EventMachine reactor needs to be running" if !EventMachine.reactor_running?
|
131
|
+
|
132
|
+
# Startup the http endpoint for /varz and /healthz
|
133
|
+
start_http_server(host, port, auth, logger)
|
134
|
+
|
135
|
+
# Listen for discovery requests
|
136
|
+
nats.subscribe('vcap.component.discover') do |msg, reply|
|
137
|
+
update_discover_uptime
|
138
|
+
nats.publish(reply, @discover.to_json)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Also announce ourselves on startup..
|
142
|
+
nats.publish('vcap.component.announce', @discover.to_json)
|
143
|
+
end
|
144
|
+
|
145
|
+
def update_discover_uptime
|
146
|
+
@discover[:uptime] = VCAP.uptime_string(Time.now - @discover[:start])
|
147
|
+
end
|
148
|
+
|
149
|
+
def clear_level(h)
|
150
|
+
h.each do |k, v|
|
151
|
+
if CONFIG_SUPPRESS.include?(k.to_sym)
|
152
|
+
h.delete(k)
|
153
|
+
else
|
154
|
+
clear_level(h[k]) if v.instance_of? Hash
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def sanitize_config(config)
|
160
|
+
# Can't Marshal/Deep Copy logger instances that services use
|
161
|
+
if config[:logger]
|
162
|
+
config = config.dup
|
163
|
+
config.delete(:logger)
|
164
|
+
end
|
165
|
+
# Deep copy
|
166
|
+
config = Marshal.load(Marshal.dump(config))
|
167
|
+
clear_level(config)
|
168
|
+
config
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
data/lib/vcap/config.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'vcap/common'
|
5
|
+
require 'vcap/json_schema'
|
6
|
+
|
7
|
+
module VCAP
|
8
|
+
class Config
|
9
|
+
class << self
|
10
|
+
attr_reader :schema
|
11
|
+
|
12
|
+
def define_schema(&blk)
|
13
|
+
@schema = VCAP::JsonSchema.build(&blk)
|
14
|
+
end
|
15
|
+
|
16
|
+
def from_file(filename, symbolize_keys=true)
|
17
|
+
config = YAML.load_file(filename)
|
18
|
+
@schema.validate(config)
|
19
|
+
config = VCAP.symbolize_keys(config) if symbolize_keys
|
20
|
+
config
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_file(config, out_filename)
|
24
|
+
@schema.validate(config)
|
25
|
+
File.open(out_filename, 'w+') do |f|
|
26
|
+
YAML.dump(config, f)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
class Fiber
|
3
|
+
attr_accessor :trace_id
|
4
|
+
alias_method :orig_resume, :resume
|
5
|
+
|
6
|
+
class << self
|
7
|
+
@io=nil
|
8
|
+
|
9
|
+
alias_method :orig_yield, :yield
|
10
|
+
|
11
|
+
def enable_tracing(io)
|
12
|
+
raise ArgumentError, "You must pass in IO object, #{io.class} given" unless io.is_a? IO
|
13
|
+
@io = io
|
14
|
+
end
|
15
|
+
|
16
|
+
def yield(*args)
|
17
|
+
log_action('yield')
|
18
|
+
begin
|
19
|
+
orig_yield(*args)
|
20
|
+
rescue FiberError => fe
|
21
|
+
Fiber.log_action('yield_error', self)
|
22
|
+
raise fe
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def log_action(action, f=nil)
|
27
|
+
return unless @io
|
28
|
+
f ||= Fiber.current
|
29
|
+
trace_id = f.trace_id || '-'
|
30
|
+
cname = Kernel.caller[1]
|
31
|
+
@io.puts("FT %-14s %-20s %-30s %s" % [action, trace_id, f.object_id, cname])
|
32
|
+
@io.flush
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def resume(*args)
|
37
|
+
Fiber.log_action('resume', self)
|
38
|
+
begin
|
39
|
+
orig_resume(*args)
|
40
|
+
rescue FiberError => fe
|
41
|
+
Fiber.log_action('resume_error', self)
|
42
|
+
raise fe
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
# This provides very basic support for creating declarative validators
|
4
|
+
# for decoded json. Useful for validating things like configs or
|
5
|
+
# messages that are published via NATS.
|
6
|
+
#
|
7
|
+
# Basic usage:
|
8
|
+
#
|
9
|
+
# schema = VCAP::JsonSchema.build do
|
10
|
+
# { :foo => [String],
|
11
|
+
# :bar => {:baz => Integer},
|
12
|
+
# optional(:jaz) => Float,
|
13
|
+
# }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Fails:
|
17
|
+
#
|
18
|
+
# schema.validate({
|
19
|
+
# 'foo' => ['bar', 5],
|
20
|
+
# 'bar' => {'baz' => 7},
|
21
|
+
# })
|
22
|
+
#
|
23
|
+
# Succeeds:
|
24
|
+
#
|
25
|
+
# schema.validate({
|
26
|
+
# 'foo' => ['bar', 'baz'],
|
27
|
+
# 'bar' => {'baz' => 7},
|
28
|
+
# })
|
29
|
+
#
|
30
|
+
#
|
31
|
+
module VCAP
|
32
|
+
module JsonSchema
|
33
|
+
|
34
|
+
class SyntaxError < StandardError; end
|
35
|
+
class ValidationError < StandardError
|
36
|
+
attr_accessor :message
|
37
|
+
def initialize(msg)
|
38
|
+
@message = msg
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
@message
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ValueError < ValidationError; end
|
47
|
+
class TypeError < ValidationError; end
|
48
|
+
class MissingKeyError < ValidationError; end
|
49
|
+
|
50
|
+
# Defines an interface that all schemas must implement
|
51
|
+
class BaseSchema
|
52
|
+
# Verfies that val conforms to the schema being validated
|
53
|
+
# Throws exceptions derived from ValidationError upon schema violations
|
54
|
+
#
|
55
|
+
# @param Object val An object decoded from json
|
56
|
+
#
|
57
|
+
# @return nil
|
58
|
+
def validate(dec_json)
|
59
|
+
raise NotImplementedError.new("You must implement validate")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class BoolSchema < BaseSchema
|
64
|
+
def validate(dec_json)
|
65
|
+
unless dec_json.kind_of?(TrueClass) || dec_json.kind_of?(FalseClass)
|
66
|
+
raise TypeError, "Expected instance of TrueClass or FalseClass, got #{dec_json.class}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Checks that supplied value is an instance of a given class
|
72
|
+
class TypeSchema < BaseSchema
|
73
|
+
def initialize(klass)
|
74
|
+
raise ArgumentError, "You must supply a class #{klass} given" unless klass.kind_of?(Class)
|
75
|
+
@klass = klass
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate(dec_json)
|
79
|
+
raise TypeError, "Expected instance of #{@klass}, got #{dec_json.class}" unless dec_json.kind_of?(@klass)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Checks that supplied value is an array, and each value in the array validates
|
84
|
+
class ArraySchema < BaseSchema
|
85
|
+
attr_accessor :item_schema
|
86
|
+
|
87
|
+
def initialize(schema)
|
88
|
+
raise ArgumentError, "You must supply a schema, #{schema.class} given" unless schema.kind_of?(BaseSchema)
|
89
|
+
@item_schema = schema
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate(dec_json)
|
93
|
+
raise TypeError, "Expected instance of Array, #{dec_json.class} given" unless dec_json.kind_of?(Array)
|
94
|
+
for v in dec_json
|
95
|
+
@item_schema.validate(v)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check that required keys are present, and that they validate
|
101
|
+
class HashSchema < BaseSchema
|
102
|
+
class Field
|
103
|
+
attr_reader :name, :schema
|
104
|
+
attr_accessor :optional
|
105
|
+
|
106
|
+
def initialize(name, schema, optional=false)
|
107
|
+
raise ArgumentError, "Schema must be an instance of a schema, #{schema.class} given #{schema.inspect}" unless schema.kind_of?(BaseSchema)
|
108
|
+
@name = name
|
109
|
+
@schema = schema
|
110
|
+
@optional = optional
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize(kvs={})
|
115
|
+
raise ArgumentError, "Expected Hash, #{kvs.class} given" unless kvs.is_a?(Hash)
|
116
|
+
# Convert symbols to strings. Validation will be performed against decoded json, which will have
|
117
|
+
# string keys.
|
118
|
+
@fields = {}
|
119
|
+
for k, v in kvs
|
120
|
+
raise ArgumentError, "Expected schema for key #{k}, got #{v.class}" unless v.kind_of?(BaseSchema)
|
121
|
+
k_s = k.to_s
|
122
|
+
@fields[k_s] = Field.new(k_s, v, false)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def required(name, schema)
|
127
|
+
name_s = name.to_s
|
128
|
+
@fields[name_s] = Field.new(name_s, schema, false)
|
129
|
+
end
|
130
|
+
|
131
|
+
def optional(name, schema)
|
132
|
+
name_s = name.to_s
|
133
|
+
@fields[name_s] = Field.new(name_s, schema, true)
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate(dec_json)
|
137
|
+
raise TypeError, "Expected instance of Hash, got instance of #{dec_json.class}" unless dec_json.kind_of?(Hash)
|
138
|
+
|
139
|
+
missing_keys = []
|
140
|
+
for k in @fields.keys
|
141
|
+
missing_keys << k unless dec_json.has_key?(k) || @fields[k].optional
|
142
|
+
end
|
143
|
+
raise MissingKeyError, "Missing keys: #{missing_keys.join(', ')}" unless missing_keys.empty?
|
144
|
+
|
145
|
+
for k, f in @fields
|
146
|
+
next if f.optional && !dec_json.has_key?(k)
|
147
|
+
begin
|
148
|
+
f.schema.validate(dec_json[k])
|
149
|
+
rescue ValidationError => ve
|
150
|
+
ve.message = "'#{k}' => " + ve.message
|
151
|
+
raise ve
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class << self
|
158
|
+
class OptionalKeyMarker
|
159
|
+
attr_reader :name
|
160
|
+
def initialize(name)
|
161
|
+
@name = name
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_s
|
165
|
+
@name.to_s
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def optional(key)
|
170
|
+
OptionalKeyMarker.new(key)
|
171
|
+
end
|
172
|
+
|
173
|
+
def build(&blk)
|
174
|
+
schema_def = instance_eval(&blk)
|
175
|
+
parse(schema_def)
|
176
|
+
end
|
177
|
+
|
178
|
+
def parse(schema_def)
|
179
|
+
case schema_def
|
180
|
+
when VCAP::JsonSchema::BaseSchema
|
181
|
+
schema_def
|
182
|
+
when Hash
|
183
|
+
schema = VCAP::JsonSchema::HashSchema.new
|
184
|
+
for k, v in schema_def
|
185
|
+
sym = k.kind_of?(OptionalKeyMarker) ? :optional : :required
|
186
|
+
schema.send(sym, k, parse(v))
|
187
|
+
end
|
188
|
+
schema
|
189
|
+
when Array
|
190
|
+
raise SyntaxError, "Schema definition for an array must have exactly 1 element" unless schema_def.size == 1
|
191
|
+
item_schema = parse(schema_def[0])
|
192
|
+
VCAP::JsonSchema::ArraySchema.new(item_schema)
|
193
|
+
when Class
|
194
|
+
VCAP::JsonSchema::TypeSchema.new(schema_def)
|
195
|
+
else
|
196
|
+
raise SyntaxError, "Don't know what to do with class #{schema_def.class}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end # VCAP::JsonSchema
|
202
|
+
end # VCAP
|