kronk 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,230 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Wraps a complex data structure to provide a search-driven interface.
5
+
6
+ class DataSet
7
+
8
+ attr_accessor :data
9
+
10
+ def initialize data
11
+ @data = data
12
+ end
13
+
14
+
15
+ ##
16
+ # Keep only specific data points from the data structure.
17
+
18
+ def collect_data_points data_paths
19
+ new_data = @data.class.new
20
+
21
+ [*data_paths].each do |data_path|
22
+ find_data data_path do |obj, k, path|
23
+
24
+ curr_data = @data
25
+ new_curr_data = new_data
26
+
27
+ path.each_with_index do |key, i|
28
+
29
+ if i == path.length - 1
30
+ new_curr_data[key] = curr_data[key]
31
+ else
32
+ new_curr_data[key] ||= curr_data[key].class.new
33
+ new_curr_data = new_curr_data[key]
34
+ curr_data = curr_data[key]
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ @data = new_data
41
+ end
42
+
43
+
44
+ ##
45
+ # Remove specific data points from the data structure.
46
+
47
+ def delete_data_points data_paths
48
+ [*data_paths].each do |data_path|
49
+ find_data data_path do |obj, k, p|
50
+ case obj
51
+ when Hash then obj.delete k
52
+ when Array then obj.delete_at k
53
+ end
54
+ end
55
+ end
56
+
57
+ @data
58
+ end
59
+
60
+
61
+ ##
62
+ # Find specific data points from a nested hash or array data structure.
63
+ # If a block is given, will pass it any matched parent data object,
64
+ # key, and full path.
65
+ #
66
+ # Data points must be an Array or String with a glob-like format.
67
+ # Special characters are: / * = | \ and are interpreted as follows:
68
+ # :key/ - walk down tree by one level from key
69
+ # :*/key - walk down tree from any parent with key as a child
70
+ # :key1|key2 - return elements with key value of key1 or key2
71
+ # :key=val - return elements where key has a value of val
72
+ # :key\* - return root-level element with key "key*"
73
+ #
74
+ # Other examples:
75
+ # find_data data, root/**=invalid|
76
+ # # Returns an Array of grand-children key/value pairs
77
+ # # where the value is 'invalid' or blank
78
+
79
+ def find_data data_path, curr_path=nil, &block
80
+ self.class.find_data @data, data_path, curr_path, &block
81
+ end
82
+
83
+
84
+ ##
85
+ # See DataSet#find_data
86
+
87
+ def self.find_data data, data_path, curr_path=nil, &block
88
+ curr_path ||= []
89
+
90
+ key, value, rec, data_path = parse_data_path data_path
91
+
92
+ yield_data_points data, key, value, rec, curr_path do |d, k, p|
93
+
94
+ if data_path
95
+ find_data d[k], data_path, p, &block
96
+ else
97
+ yield d, k, p
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ ##
104
+ # Parses a given data point and returns an array with the following:
105
+ # - Key to match
106
+ # - Value to match
107
+ # - Recursive matching
108
+ # - New data path value
109
+
110
+ def self.parse_data_path data_path
111
+ data_path = data_path.dup
112
+ key = nil
113
+ value = nil
114
+ recursive = false
115
+
116
+ until key && key != "**" || value || data_path.empty? do
117
+ value = data_path.slice!(%r{((.*?[^\\])+?/)})
118
+ (value ||= data_path).sub!(/\/$/, '')
119
+
120
+ data_path = nil if value == data_path
121
+
122
+ key = value.slice! %r{((.*?[^\\])+?=)}
123
+ key, value = value, nil if key.nil?
124
+ key.sub!(/\=$/, '')
125
+
126
+ value = parse_path_item value if value
127
+
128
+ if key =~ /^\*{2,}$/
129
+ if data_path && !value
130
+ key, value, rec, data_path = parse_data_path(data_path)
131
+ else
132
+ key = /.*/
133
+ end
134
+
135
+ recursive = true
136
+ else
137
+ key = parse_path_item key
138
+ end
139
+ end
140
+
141
+ data_path = nil if data_path && data_path.empty?
142
+
143
+ [key, value, recursive, data_path]
144
+ end
145
+
146
+
147
+ ##
148
+ # Decide whether to make path item a regex or not.
149
+
150
+ def self.parse_path_item str
151
+ if str =~ /(^|[^\\])([\*\?\|])/
152
+ str.gsub!(/(^|[^\\])(\*|\?)/, '\1.\2')
153
+ str = /^(#{str})$/
154
+ else
155
+ str.gsub! "\\", ""
156
+ end
157
+
158
+ str
159
+ end
160
+
161
+
162
+ ##
163
+ # Yield data object and key, if a specific key or value matches
164
+ # the given data.
165
+
166
+ def self.yield_data_points data, mkey, mvalue=nil,
167
+ recursive=false, path=nil, &block
168
+
169
+ return unless Hash === data || Array === data
170
+ path ||= []
171
+
172
+ each_data_item data do |key, value|
173
+ curr_path = path.dup << key
174
+
175
+ found = match_data_item(mkey, key) &&
176
+ match_data_item(mvalue, value)
177
+
178
+ yield data, key, curr_path if found
179
+ yield_data_points data[key], mkey, mvalue, true, curr_path, &block if
180
+ recursive
181
+ end
182
+ end
183
+
184
+
185
+ ##
186
+ # Check if data key or value is a match for nested data searches.
187
+
188
+ def self.match_data_item item1, item2
189
+ return if !item1.nil? && (Array === item2 || Hash === item2)
190
+
191
+ if Regexp === item1
192
+ item2.to_s =~ item1
193
+ elsif item1.nil?
194
+ true
195
+ else
196
+ item2.to_s == item1.to_s
197
+ end
198
+ end
199
+
200
+
201
+ ##
202
+ # Universal iterator for Hash and Array objects.
203
+
204
+ def self.each_data_item data, &block
205
+ case data
206
+
207
+ when Hash
208
+ data.each(&block)
209
+
210
+ when Array
211
+ i = 0
212
+
213
+ # We need to iterate through the array this way
214
+ # in case items in it get deleted.
215
+
216
+ while i < data.length do
217
+ index = i
218
+ old_length = data.length
219
+
220
+ block.call index, data[index]
221
+
222
+ adj = old_length - data.length
223
+ adj = 0 if adj < 0
224
+
225
+ i = i.next - adj
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
data/lib/kronk/diff.rb ADDED
@@ -0,0 +1,210 @@
1
+ class Kronk
2
+
3
+
4
+ ##
5
+ # Creates simple diffs as formatted strings or arrays, from two strings or
6
+ # data objects.
7
+
8
+ class Diff
9
+
10
+
11
+ ##
12
+ # Creates a new diff from two data objects.
13
+
14
+ def self.new_from_data data1, data2, options={}
15
+ new ordered_data_string(data1, options[:struct]),
16
+ ordered_data_string(data2, options[:struct])
17
+ end
18
+
19
+
20
+ ##
21
+ # Returns a data string that is diff-able, meaning sorted by
22
+ # Hash keys when available.
23
+
24
+ def self.ordered_data_string data, struct_only=false, indent=0
25
+ case data
26
+
27
+ when Hash
28
+ output = "{\n"
29
+
30
+ key_width = 0
31
+ data.keys.each do |k|
32
+ key_width = k.inspect.length if k.inspect.length > key_width
33
+ end
34
+
35
+ data_values =
36
+ data.map do |key, value|
37
+ pad = " " * indent
38
+ subdata = ordered_data_string value, struct_only, indent + 1
39
+ "#{pad}#{key.inspect} => #{subdata}"
40
+ end
41
+
42
+ output << data_values.sort.join(",\n") << "\n"
43
+ output << "#{" " * indent}}"
44
+
45
+ when Array
46
+ output = "[\n"
47
+
48
+ data_values =
49
+ data.map do |value|
50
+ pad = " " * indent
51
+ "#{pad}#{ordered_data_string value, struct_only, indent + 1}"
52
+ end
53
+
54
+ output << data_values.join(",\n") << "\n"
55
+ output << "#{" " * indent}]"
56
+
57
+ else
58
+ return data.inspect unless struct_only
59
+ return "Boolean" if data == true || data == false
60
+ data.class
61
+ end
62
+ end
63
+
64
+
65
+ attr_accessor :str1, :str2, :char, :format
66
+
67
+ def initialize str1, str2, char=/\r?\n/
68
+ @str1 = str1
69
+ @str2 = str2
70
+ @char = char
71
+ @diff_ary = nil
72
+ @format = Kronk.config[:diff_format] || :ascii_diff
73
+ end
74
+
75
+
76
+ ##
77
+ # Returns a diff array with the following format:
78
+ # str1 = "match1\nmatch2\nstr1 val"
79
+ # str1 = "match1\nin str2\nmore str2\nmatch2\nstr2 val"
80
+ #
81
+ # Diff.new(str1, str2).create_diff
82
+ # ["match 1",
83
+ # [[], ["in str2", "more str2"]],
84
+ # "match 2",
85
+ # [["str1 val"], ["str2 val"]]]
86
+
87
+ def create_diff
88
+ diff_ary = []
89
+ sub_diff = nil
90
+
91
+ arr1 = @str1.split @char
92
+ arr2 = @str2.split @char
93
+
94
+ until arr1.empty? && arr2.empty?
95
+ item1, item2 = arr1.shift, arr2.shift
96
+
97
+ if item1 == item2
98
+ if sub_diff
99
+ diff_ary << sub_diff
100
+ sub_diff = nil
101
+ end
102
+
103
+ diff_ary << item1
104
+ next
105
+ end
106
+
107
+ match1 = arr1.index item2
108
+ match2 = arr2.index item1
109
+
110
+ if match1
111
+ diff_ary.concat diff_match(item1, match1, arr1, 0, sub_diff)
112
+ sub_diff = nil
113
+
114
+ elsif match2
115
+ diff_ary.concat diff_match(item2, match2, arr2, 1, sub_diff)
116
+ sub_diff = nil
117
+
118
+ elsif !item1.nil? || !item2.nil?
119
+ sub_diff ||= [[],[]]
120
+ sub_diff[0] << item1 if item1
121
+ sub_diff[1] << item2 if item2
122
+ end
123
+ end
124
+
125
+ diff_ary << sub_diff if sub_diff
126
+
127
+ diff_ary
128
+ end
129
+
130
+
131
+ ##
132
+ # Create a diff from a match.
133
+
134
+ def diff_match item, match, arr, side, sub_diff
135
+ sub_diff ||= [[],[]]
136
+
137
+ index = match - 1
138
+ added = [item]
139
+ added.concat arr.slice!(0..index) if index >= 0
140
+
141
+ sub_diff[side].concat(added)
142
+ [sub_diff, arr.shift]
143
+ end
144
+
145
+
146
+ ##
147
+ # Returns a formatted output as a string.
148
+ # Custom formats may be achieved by passing a block.
149
+
150
+ def formatted format=@format, join_char="\n", &block
151
+ block ||= method format
152
+
153
+ diff_array.map do |item|
154
+ block.call item.dup
155
+ end.flatten.join join_char
156
+ end
157
+
158
+
159
+ ##
160
+ # Formats a single diff element to the default diff format.
161
+
162
+ def ascii_diff item
163
+ case item
164
+ when String
165
+ " #{item}"
166
+ when Array
167
+ item[0] = item[0].map{|str| "- #{str}"}
168
+ item[1] = item[1].map{|str| "+ #{str}"}
169
+ item
170
+ else
171
+ " #{item.inspect}"
172
+ end
173
+ end
174
+
175
+
176
+ ##
177
+ # Formats a single diff element with colors.
178
+
179
+ def color_diff item
180
+ case item
181
+ when String
182
+ item
183
+ when Array
184
+ item[0] = item[0].map{|str| "\033[31m#{str}\033[0m"}
185
+ item[1] = item[1].map{|str| "\033[32m#{str}\033[0m"}
186
+ item
187
+ else
188
+ item.inspect
189
+ end
190
+ end
191
+
192
+
193
+ ##
194
+ # Returns the cached diff array when available, otherwise creates it.
195
+
196
+ def diff_array
197
+ @diff_ary ||= create_diff
198
+ end
199
+
200
+ alias to_a diff_array
201
+
202
+
203
+ ##
204
+ # Returns a diff string with the default format.
205
+
206
+ def to_s
207
+ formatted
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,15 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Simple plist wrapper to add :parse method.
5
+
6
+ class PlistParser
7
+
8
+ ##
9
+ # Alias for Plist.parse_xml
10
+
11
+ def self.parse plist
12
+ Plist.parse_xml plist
13
+ end
14
+ end
15
+ end