kronk 1.0.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.
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