vcap_common 1.0.10
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/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
|