kronk 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2010-11-19
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,14 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/kronk
7
+ lib/kronk.rb
8
+ lib/kronk/data_set.rb
9
+ lib/kronk/diff.rb
10
+ lib/kronk/plist_parser.rb
11
+ lib/kronk/request.rb
12
+ lib/kronk/response.rb
13
+ lib/kronk/xml_parser.rb
14
+ test/test_kronk.rb
data/README.txt ADDED
@@ -0,0 +1,118 @@
1
+ = kronk
2
+
3
+ * https://github.com/yaksnrainbows/kronk
4
+
5
+ == DESCRIPTION:
6
+
7
+ Kronk runs diffs against data from live and cached http responses.
8
+ Kronk was made possible by the sponsoring of AT&T Interactive.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ * Parse and diff data from http response body and/or headers.
13
+
14
+ * Include or exclude particular data points.
15
+
16
+ * Supports json, rails-ish xml, and plist.
17
+
18
+ * Support for custom data parsers.
19
+
20
+ == FUTURE:
21
+
22
+ * Line numbered and custom diff output.
23
+
24
+ * Auto-queryer with optional param randomizing.
25
+
26
+ * Support for test suites.
27
+
28
+ * Support for proxies, ssl, and http auth.
29
+
30
+ == SYNOPSIS:
31
+
32
+ Check if your json response returns the same data as your xml variety:
33
+
34
+ $ kronk http://host.com/path.json http://host.com/path.xml
35
+
36
+ Compare headers only but only content type and date:
37
+
38
+ $ kronk http://host.com/path1 http://host.com/path2 -I Content-Type,Date
39
+
40
+ Compare body structure only:
41
+
42
+ $ kronk http://host.com/path.json http://host.com/path.xml --struct
43
+
44
+ Call comparison with similar uri suffixes:
45
+
46
+ $ kronk http://host.com/path.json http://host.com/path.xml --suff '?page=1'
47
+
48
+ Compare body and headers:
49
+
50
+ $ kronk http://host.com/path.json http://host.com/path.xml -i
51
+
52
+ Compare body and content type header:
53
+
54
+ $ kronk http://host.com/path.json http://host.com/path.xml -i Content-Type
55
+
56
+ Compare response with the previous call:
57
+
58
+ $ kronk http://host.com/path --prev
59
+
60
+ Compare response with a local file:
61
+
62
+ $ kronk http://host.com/path.json ./saved_response.json
63
+
64
+ Do a simple text diff on the http response, including headers:
65
+
66
+ $ kronk http://host.com/A/path.json http://host.com/B/path.json --raw -i
67
+
68
+ Run it to display formatted data:
69
+
70
+ $ kronk http://host.com/path.json
71
+
72
+ $ kronk http://host.com/path.json -i
73
+
74
+ Run it to display raw data with headers:
75
+
76
+ $ kronk http://host.com/path.json --raw -i
77
+
78
+ == REQUIREMENTS:
79
+
80
+ * FIX (list of requirements)
81
+
82
+ == INSTALL:
83
+
84
+ * sudo gem install kronk
85
+
86
+ == DEVELOPERS:
87
+
88
+ After checking out the source, run:
89
+
90
+ $ rake newb
91
+
92
+ This task will install any missing dependencies, run the tests/specs,
93
+ and generate the RDoc.
94
+
95
+ == LICENSE:
96
+
97
+ (The MIT License)
98
+
99
+ Copyright (c) 2010 Jeremie Castagna
100
+
101
+ Permission is hereby granted, free of charge, to any person obtaining
102
+ a copy of this software and associated documentation files (the
103
+ 'Software'), to deal in the Software without restriction, including
104
+ without limitation the rights to use, copy, modify, merge, publish,
105
+ distribute, sublicense, and/or sell copies of the Software, and to
106
+ permit persons to whom the Software is furnished to do so, subject to
107
+ the following conditions:
108
+
109
+ The above copyright notice and this permission notice shall be
110
+ included in all copies or substantial portions of the Software.
111
+
112
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
113
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
114
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
115
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
116
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
117
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
118
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.plugin :isolate
7
+
8
+ Hoe.spec 'kronk' do
9
+ developer('Jeremie Castagna', 'yaksnrainbows@gmail.com')
10
+
11
+ self.extra_deps << ['plist', '>=3.1.0']
12
+ self.extra_deps << ['json', '>=1.2.0']
13
+ self.extra_deps << ['nokogiri', '>=1.3.3']
14
+ self.extra_deps << ['i18n', '>=0.5.0']
15
+ self.extra_deps << ['activesupport', '>=2.0.0']
16
+
17
+ self.extra_dev_deps << ['mocha', '>=0.9.10']
18
+ end
19
+
20
+ # vim: syntax=ruby
data/bin/kronk ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'kronk'
5
+
6
+ rescue LoadError => e
7
+ raise unless e.message =~ %r{no such file to load -- kronk}
8
+
9
+ $: << File.join(File.dirname(__FILE__), "../lib")
10
+ require 'kronk'
11
+ end
12
+
13
+ Kronk.run
data/lib/kronk.rb ADDED
@@ -0,0 +1,404 @@
1
+ require 'rubygems'
2
+ require 'plist'
3
+ require 'json'
4
+ require 'nokogiri'
5
+
6
+ # Support for new and old versions of ActiveSupport
7
+ begin
8
+ require 'active_support/inflector'
9
+ rescue LoadError
10
+ require 'activesupport'
11
+ end
12
+
13
+ require 'net/http'
14
+ require 'optparse'
15
+
16
+ class Kronk
17
+
18
+ # This gem's version.
19
+ VERSION = '1.0.0'
20
+
21
+
22
+ require 'kronk/data_set'
23
+ require 'kronk/diff'
24
+ require 'kronk/response'
25
+ require 'kronk/request'
26
+ require 'kronk/plist_parser'
27
+ require 'kronk/xml_parser'
28
+
29
+
30
+ # Default config file to load. Defaults to ~/.kronk.
31
+ DEFAULT_CONFIG_FILE = File.expand_path "~/.kronk"
32
+
33
+
34
+ # Default cache file.
35
+ DEFAULT_CACHE_FILE = File.expand_path "~/.kronk_cache"
36
+
37
+
38
+ # Default Content-Type header to parser mapping.
39
+ DEFAULT_CONTENT_TYPES = {
40
+ 'js' => 'JSON',
41
+ 'json' => 'JSON',
42
+ 'plist' => 'PlistParser',
43
+ 'xml' => 'XMLParser'
44
+ }
45
+
46
+
47
+ # Default config to use.
48
+ DEFAULT_CONFIG = {
49
+ :content_types => DEFAULT_CONTENT_TYPES.dup,
50
+ :diff_format => :ascii_diff,
51
+ :cache_file => DEFAULT_CACHE_FILE,
52
+ :requires => []
53
+ }
54
+
55
+
56
+ ##
57
+ # Read the Kronk config hash.
58
+
59
+ def self.config
60
+ @config ||= DEFAULT_CONFIG
61
+ end
62
+
63
+
64
+ ##
65
+ # Load a config file and apply to Kronk.config.
66
+
67
+ def self.load_config filepath=DEFAULT_CONFIG_FILE
68
+ conf = YAML.load_file DEFAULT_CONFIG_FILE
69
+ content_types = conf.delete :content_types
70
+
71
+ if conf[:requires]
72
+ requires = [*conf.delete(:requires)]
73
+ self.config[:requires] ||= []
74
+ self.config[:requires].concat requires
75
+ end
76
+
77
+ self.config[:content_types].merge!(content_types) if content_types
78
+ self.config.merge! conf
79
+ end
80
+
81
+
82
+ ##
83
+ # Load the config-based requires.
84
+
85
+ def self.load_requires
86
+ return unless config[:requires]
87
+ config[:requires].each{|lib| require lib }
88
+ end
89
+
90
+
91
+ ##
92
+ # Creates the default config file at the given path.
93
+
94
+ def self.make_config_file filepath=DEFAULT_CONFIG_FILE
95
+ File.open filepath, "w+" do |file|
96
+ file << DEFAULT_CONFIG.to_yaml
97
+ end
98
+ end
99
+
100
+
101
+ ##
102
+ # Find a fully qualified ruby namespace/constant.
103
+
104
+ def self.find_const namespace
105
+ consts = namespace.split "::"
106
+ curr = self
107
+
108
+ until consts.empty? do
109
+ curr = curr.const_get consts.shift
110
+ end
111
+
112
+ curr
113
+ end
114
+
115
+
116
+ ##
117
+ # Returns the config-defined parser class for a given content type.
118
+
119
+ def self.parser_for content_type
120
+ parser_pair =
121
+ config[:content_types].select do |key, value|
122
+ (content_type =~ %r{#{key}}) && value
123
+ end
124
+
125
+ return if parser_pair.empty?
126
+
127
+ parser = parser_pair[0][1]
128
+ parser = find_const parser if String === parser || Symbol === parser
129
+ parser
130
+ end
131
+
132
+
133
+ ##
134
+ # Make requests, parse the responses and compare the data.
135
+ # If the second argument is omitted or is passed :cache, will
136
+ # attempt to compare with the last made request. If there was no last
137
+ # request will compare against nil.
138
+ #
139
+ # Supports the following options:
140
+ # :data:: Hash/String - the data to pass to the http request
141
+ # :headers:: Hash - extra headers to pass to the request
142
+ # :http_method:: Symbol - the http method to use; defaults to :get
143
+ # :ignore_data:: String/Array - defines which data points to exclude
144
+ # :with_headers:: Bool/String/Array - defines which headers to include
145
+ # :raw:: Bool - run diff on raw strings
146
+ #
147
+ # Returns a diff object.
148
+
149
+ def self.compare query1, query2=:cache, options={}
150
+ diff =
151
+ if options[:raw]
152
+ raw_diff query1, query2, options
153
+ else
154
+ data_diff query1, query2, options
155
+ end
156
+
157
+ diff
158
+ end
159
+
160
+
161
+ ##
162
+ # Return a diff object from two responses' raw data.
163
+
164
+ def self.raw_diff query1, query2, options={}
165
+ resp1 = Request.retrieve query1, options
166
+ resp2 = Request.retrieve query2, options
167
+
168
+ Diff.new resp1.selective_string(options), resp2.selective_string(options)
169
+ end
170
+
171
+
172
+ ##
173
+ # Return a diff object from two parsed responses.
174
+
175
+ def self.data_diff query1, query2, options={}
176
+ resp1 = Request.retrieve query1, options
177
+ resp2 = Request.retrieve query2, options
178
+
179
+ Diff.new_from_data resp1.selective_data(options),
180
+ resp2.selective_data(options),
181
+ options
182
+ end
183
+
184
+
185
+ ##
186
+ # Runs the kronk command with the given terminal args.
187
+
188
+ def self.run argv=ARGV
189
+ begin
190
+ load_config
191
+
192
+ rescue Errno::ENOENT
193
+ make_config_file
194
+
195
+ $stderr << "\nNo config file was found.\n\n"
196
+ $stderr << "Created default config in #{DEFAULT_CONFIG_FILE}\n"
197
+ $stderr << "Edit file if necessary and try again.\n"
198
+ exit 1
199
+ end
200
+
201
+ options = parse_args argv
202
+
203
+ config[:requires].concat options[:requires] if options[:requires]
204
+ load_requires
205
+
206
+ options[:cache_response] = config[:cache_file] if config[:cache_file]
207
+
208
+ uri1, uri2 = options.delete :uris
209
+
210
+ if uri1 && uri2
211
+ diff = compare uri1, uri2, options
212
+ puts diff.formatted(config[:diff_format])
213
+
214
+ elsif options[:raw]
215
+ puts Request.retrieve(uri1, options).selective_string(options)
216
+
217
+ else
218
+ data = Request.retrieve(uri1, options).selective_data options
219
+ puts Diff.ordered_data_string(data, options[:struct])
220
+ end
221
+
222
+ rescue Request::NotFoundError, Response::MissingParser => e
223
+ $stderr << "\nError: #{e.message}\n"
224
+ exit 2
225
+ end
226
+
227
+
228
+ ##
229
+ # Parse ARGV
230
+
231
+ def self.parse_args argv
232
+ options = {
233
+ :with_headers => false,
234
+ :no_body => false,
235
+ :uris => []
236
+ }
237
+
238
+ options[:only_data], options[:ignore_data] = parse_data_path_args argv
239
+
240
+ opts = OptionParser.new do |opt|
241
+ opt.program_name = File.basename $0
242
+ opt.version = VERSION
243
+ opt.release = nil
244
+
245
+ opt.banner = <<-STR
246
+ Kronk runs diffs against data from live and cached http responses.
247
+
248
+ Usage:
249
+ #{opt.program_name} --help
250
+ #{opt.program_name} --version
251
+ #{opt.program_name} uri1 [uri2] [options...] [-- data-paths]
252
+
253
+ Examples:
254
+ #{opt.program_name} http://example.com/A
255
+ #{opt.program_name} http://example.com/B --prev --raw
256
+ #{opt.program_name} http://example.com/B.xml local/file/B.json
257
+ #{opt.program_name} file1.json file2.json -- **/key1=val1 -root/key?
258
+
259
+ Arguments after -- will be used to focus the diff on specific data points.
260
+ If the data paths start with a '-' the matched data points will be removed.
261
+
262
+ Options:
263
+ STR
264
+
265
+ opt.on('-d', '--data STR', String,
266
+ 'Post data with the request') do |value|
267
+ options[:data] = value
268
+ options[:http_method] ||= 'POST'
269
+ end
270
+
271
+
272
+ opt.on('--prev', 'Use last response to diff against') do
273
+ options[:uris] << :cache
274
+ end
275
+
276
+
277
+ opt.on('--suff STR', String,
278
+ 'Add common path items to the end of each URI') do |value|
279
+ options[:uri_suffix] = value
280
+ end
281
+
282
+
283
+ opt.on('-H', '--header STR', String,
284
+ 'Header to pass to the server request') do |value|
285
+ options[:headers] ||= {}
286
+
287
+ key, value = value.split ": ", 2
288
+ options[:headers][key] = value
289
+ end
290
+
291
+
292
+ opt.on('-i', '--include [header1,header2]', Array,
293
+ 'Include all or given headers in response') do |value|
294
+ options[:with_headers] ||= []
295
+
296
+ if value
297
+ options[:with_headers].concat value if
298
+ Array === options[:with_headers]
299
+ else
300
+ options[:with_headers] = true
301
+ end
302
+ end
303
+
304
+
305
+ opt.on('-I', '--head [header1,header2]', Array,
306
+ 'Use all or given headers only in the response') do |value|
307
+ options[:with_headers] ||= []
308
+
309
+ if value
310
+ options[:with_headers].concat value if
311
+ Array === options[:with_headers]
312
+ else
313
+ options[:with_headers] = true
314
+ end
315
+
316
+ options[:no_body] = true
317
+ end
318
+
319
+
320
+ opt.on('-L', '--location [NUM]', Integer,
321
+ 'Follow the location header always or num times') do |value|
322
+ options[:follow_redirects] = value || true
323
+ end
324
+
325
+
326
+ opt.on('-X', '--request STR', String,
327
+ 'The request method to use') do |value|
328
+ options[:http_method] = value
329
+ end
330
+
331
+
332
+ opt.on('-r', '--require lib1,lib2', Array,
333
+ 'Require a library or gem') do |value|
334
+ options[:requires] ||= []
335
+ options[:requires].concat value
336
+ end
337
+
338
+
339
+ opt.on('--raw', 'Run diff on the raw data returned') do
340
+ options[:raw] = true
341
+ end
342
+
343
+
344
+ opt.on('--struct', 'Run diff on the data structure') do
345
+ options[:struct] = true
346
+ end
347
+
348
+
349
+ opt.on('--ascii', 'Return ascii formatted diff') do
350
+ config[:diff_format] = :ascii_diff
351
+ end
352
+
353
+
354
+ opt.on('--color', 'Return color formatted diff') do
355
+ config[:diff_format] = :color_diff
356
+ end
357
+
358
+
359
+ #opt.on('-v', '--verbose', 'Make the operation more talkative') do
360
+ # options[:verbose] = true
361
+ #end
362
+
363
+ opt.separator nil
364
+ end
365
+
366
+ opts.parse! argv
367
+
368
+ options[:uris].concat argv
369
+ options[:uris].slice!(2..-1)
370
+
371
+ if options[:uris].empty?
372
+ $stderr << "\nError: You must enter at least one URI\n\n"
373
+ $stderr << opts.to_s
374
+ exit 1
375
+ end
376
+
377
+ options
378
+ end
379
+
380
+
381
+ ##
382
+ # Searches ARGV and returns data paths to add or exclude in the diff.
383
+ # Returns the array [only_paths, except_paths]
384
+
385
+ def self.parse_data_path_args argv
386
+ return unless argv.include? "--"
387
+
388
+ data_paths = argv.slice! argv.index("--")..-1
389
+ data_paths.shift
390
+
391
+ only_paths = nil
392
+ except_paths = nil
393
+
394
+ data_paths.each do |path|
395
+ if path[0,1] == "-"
396
+ (except_paths ||= []) << path[1..-1]
397
+ else
398
+ (only_paths ||= []) << path
399
+ end
400
+ end
401
+
402
+ [only_paths, except_paths]
403
+ end
404
+ end