json_select 0.1.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.
Files changed (75) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +7 -0
  3. data/LICENSE +20 -0
  4. data/README.md +67 -0
  5. data/Rakefile +18 -0
  6. data/json_select.gemspec +23 -0
  7. data/lib/json_select.rb +35 -0
  8. data/lib/json_select/ast/combination_selector.rb +14 -0
  9. data/lib/json_select/ast/complex_expr.rb +27 -0
  10. data/lib/json_select/ast/even_expr.rb +7 -0
  11. data/lib/json_select/ast/hash_selector.rb +12 -0
  12. data/lib/json_select/ast/odd_expr.rb +7 -0
  13. data/lib/json_select/ast/pseudo_selector.rb +24 -0
  14. data/lib/json_select/ast/selector_group.rb +17 -0
  15. data/lib/json_select/ast/simple_expr.rb +7 -0
  16. data/lib/json_select/ast/simple_selector.rb +23 -0
  17. data/lib/json_select/ast/type_selector.rb +8 -0
  18. data/lib/json_select/ast/universal_selector.rb +7 -0
  19. data/lib/json_select/parser.rb +19 -0
  20. data/lib/json_select/selector.rb +176 -0
  21. data/lib/json_select/selector_parser.rb +1251 -0
  22. data/lib/json_select/selector_parser.tt +161 -0
  23. data/lib/json_select/version.rb +3 -0
  24. data/spec/conformance_spec.rb +41 -0
  25. data/spec/fixtures/README.md +14 -0
  26. data/spec/fixtures/alltests.txt +31 -0
  27. data/spec/fixtures/basic.json +31 -0
  28. data/spec/fixtures/basic.xml +31 -0
  29. data/spec/fixtures/basic_first-child.ast +6 -0
  30. data/spec/fixtures/basic_first-child.output +6 -0
  31. data/spec/fixtures/basic_first-child.selector +2 -0
  32. data/spec/fixtures/basic_grouping.ast +12 -0
  33. data/spec/fixtures/basic_grouping.output +4 -0
  34. data/spec/fixtures/basic_grouping.selector +1 -0
  35. data/spec/fixtures/basic_id.ast +3 -0
  36. data/spec/fixtures/basic_id.output +1 -0
  37. data/spec/fixtures/basic_id.selector +1 -0
  38. data/spec/fixtures/basic_id_multiple.ast +3 -0
  39. data/spec/fixtures/basic_id_multiple.output +3 -0
  40. data/spec/fixtures/basic_id_multiple.selector +1 -0
  41. data/spec/fixtures/basic_id_quotes.ast +3 -0
  42. data/spec/fixtures/basic_id_quotes.output +2 -0
  43. data/spec/fixtures/basic_id_quotes.selector +1 -0
  44. data/spec/fixtures/basic_id_with_type.ast +4 -0
  45. data/spec/fixtures/basic_id_with_type.output +1 -0
  46. data/spec/fixtures/basic_id_with_type.selector +1 -0
  47. data/spec/fixtures/basic_last-child.ast +6 -0
  48. data/spec/fixtures/basic_last-child.output +6 -0
  49. data/spec/fixtures/basic_last-child.selector +2 -0
  50. data/spec/fixtures/basic_nth-child-2.ast +6 -0
  51. data/spec/fixtures/basic_nth-child-2.output +13 -0
  52. data/spec/fixtures/basic_nth-child-2.selector +1 -0
  53. data/spec/fixtures/basic_nth-child.ast +6 -0
  54. data/spec/fixtures/basic_nth-child.output +7 -0
  55. data/spec/fixtures/basic_nth-child.selector +1 -0
  56. data/spec/fixtures/basic_nth-last-child.ast +6 -0
  57. data/spec/fixtures/basic_nth-last-child.output +6 -0
  58. data/spec/fixtures/basic_nth-last-child.selector +1 -0
  59. data/spec/fixtures/basic_root_pseudo.ast +3 -0
  60. data/spec/fixtures/basic_root_pseudo.output +31 -0
  61. data/spec/fixtures/basic_root_pseudo.selector +1 -0
  62. data/spec/fixtures/basic_type.ast +3 -0
  63. data/spec/fixtures/basic_type.output +14 -0
  64. data/spec/fixtures/basic_type.selector +1 -0
  65. data/spec/fixtures/basic_type2.ast +3 -0
  66. data/spec/fixtures/basic_type2.output +1 -0
  67. data/spec/fixtures/basic_type2.selector +1 -0
  68. data/spec/fixtures/basic_type3.ast +3 -0
  69. data/spec/fixtures/basic_type3.output +47 -0
  70. data/spec/fixtures/basic_type3.selector +1 -0
  71. data/spec/fixtures/basic_universal.ast +2 -0
  72. data/spec/fixtures/basic_universal.output +85 -0
  73. data/spec/fixtures/basic_universal.selector +1 -0
  74. data/spec/spec_helper.rb +6 -0
  75. metadata +190 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in json_select.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
7
+ gem 'yajl-ruby'
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Simon Menke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # JSONSelect
2
+
3
+ **CSS-like selectors for JSON.**
4
+
5
+ [More info about the JSON:select format](http://jsonselect.org/)
6
+
7
+ ## Installation
8
+
9
+ From your terminal:
10
+
11
+ ```bash
12
+ gem install json_select
13
+ ```
14
+
15
+ In your `Gemfile`:
16
+
17
+ ```ruby
18
+ gem 'json_select'
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ require 'json_select'
25
+
26
+ json = { # This would normally be loaded with something like yajl-ruby
27
+ "name" => {
28
+ "first" => "Lloyd",
29
+ "last" => "Hilaiel"
30
+ },
31
+ "favoriteColor" => "yellow",
32
+ "languagesSpoken" => [
33
+ {
34
+ "language" => "Bulgarian",
35
+ "level" => "advanced"
36
+ },
37
+ {
38
+ "language" => "English",
39
+ "level" => "native"
40
+ },
41
+ {
42
+ "language" => "Spanish",
43
+ "level" => "beginner"
44
+ }
45
+ ],
46
+ "seatingPreference" => [ "window", "aisle" ],
47
+ "drinkPreference" => [ "beer", "whiskey", "wine" ],
48
+ "weight" => 172
49
+ }
50
+
51
+ JSONSelect('string:first-child').match(json) # => ["Lloyd", "Bulgarian", "English", "Spanish", "window", "beer"]
52
+ ```
53
+
54
+ ## Note on Patches/Pull Requests
55
+
56
+ * Fork the project.
57
+ * Make your feature addition or bug fix.
58
+ * Add tests for it. This is important so I don't break it in a future version
59
+ unintentionally.
60
+ * Commit, do not mess with `Rakefile` or version. (if you want to have your
61
+ own version, that is fine but bump version in a commit by itself I can
62
+ ignore when I pull)
63
+ * Send me a pull request. Bonus points for topic branches.
64
+
65
+ ## Copyright
66
+
67
+ Copyright (c) 2011 Simon Menke. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.pattern = "./spec/**/*_spec.rb"
7
+ t.rspec_opts = [
8
+ '--color',
9
+ '-r', File.expand_path("../spec/spec_helper.rb", __FILE__)]
10
+ end
11
+
12
+ task :build_parser do
13
+ sh "bundle exec tt lib/json_select/selector_parser.tt -o lib/json_select/selector_parser.rb"
14
+ end
15
+
16
+ task :spec => :build_parser
17
+ task :build => :build_parser
18
+ task :default => :spec
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "json_select/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "json_select"
7
+ s.version = JSONSelect::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Simon Menke"]
10
+ s.email = ["simon.menke@gmail.com"]
11
+ s.homepage = "http://github.com/fd/json_select"
12
+ s.summary = %q{JSONSelect implementation for Ruby}
13
+ s.description = %q{JSONSelect implementation for Ruby}
14
+
15
+ s.rubyforge_project = "json_select"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_runtime_dependency 'treetop'
23
+ end
@@ -0,0 +1,35 @@
1
+ module JSONSelect
2
+
3
+ require 'treetop'
4
+
5
+ require 'json_select/version'
6
+ require 'json_select/selector_parser'
7
+ require 'json_select/selector'
8
+ require 'json_select/parser'
9
+
10
+ module Ast
11
+ require 'json_select/ast/combination_selector'
12
+ require 'json_select/ast/simple_selector'
13
+ require 'json_select/ast/selector_group'
14
+ require 'json_select/ast/type_selector'
15
+ require 'json_select/ast/hash_selector'
16
+ require 'json_select/ast/pseudo_selector'
17
+ require 'json_select/ast/universal_selector'
18
+
19
+ require 'json_select/ast/odd_expr'
20
+ require 'json_select/ast/even_expr'
21
+ require 'json_select/ast/simple_expr'
22
+ require 'json_select/ast/complex_expr'
23
+ end
24
+
25
+ ParseError = Class.new(RuntimeError)
26
+
27
+ def self.parse(selector)
28
+ JSONSelect::Parser.new(selector).parse
29
+ end
30
+
31
+ end
32
+
33
+ def JSONSelect(selector)
34
+ JSONSelect.parse(selector)
35
+ end
@@ -0,0 +1,14 @@
1
+ module JSONSelect::Ast::CombinationSelector
2
+
3
+ def to_ast
4
+ ast = [a.to_ast]
5
+
6
+ b.elements.each do |comb|
7
+ ast.push('>') if comb.c.text_value.strip == '>'
8
+ ast.push(comb.d.to_ast)
9
+ end
10
+
11
+ ast
12
+ end
13
+
14
+ end
@@ -0,0 +1,27 @@
1
+ module JSONSelect::Ast::ComplexExpr
2
+
3
+ def to_ast
4
+ _a = ""
5
+
6
+ if a.text_value.size > 0
7
+ _a += a.text_value
8
+ else
9
+ _a += '+'
10
+ end
11
+
12
+ if b.text_value.size > 0
13
+ _a += b.text_value
14
+ else
15
+ _a += '1'
16
+ end
17
+
18
+ if c.text_value.size > 0
19
+ _b = c.text_value.to_i
20
+ else
21
+ _b = 0
22
+ end
23
+
24
+ { 'a' => _a.to_i, 'b' => _b }
25
+ end
26
+
27
+ end
@@ -0,0 +1,7 @@
1
+ module JSONSelect::Ast::EvenExpr
2
+
3
+ def to_ast
4
+ { 'a' => 2, 'b' => 0 }
5
+ end
6
+
7
+ end
@@ -0,0 +1,12 @@
1
+ module JSONSelect::Ast::HashSelector
2
+
3
+ def to_ast
4
+ tv = self.text_value[1..-1]
5
+ if tv[0,1] == '"'
6
+ { "class" => eval(tv) }
7
+ else
8
+ { "class" => tv }
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,7 @@
1
+ module JSONSelect::Ast::OddExpr
2
+
3
+ def to_ast
4
+ { 'a' => 2, 'b' => 1 }
5
+ end
6
+
7
+ end
@@ -0,0 +1,24 @@
1
+ module JSONSelect::Ast::PseudoSelector
2
+
3
+ def to_ast
4
+ if respond_to?(:e)
5
+ ast = { "pseudo_function" => a.text_value, 'a' => 0 , 'b' => 0 }
6
+ ast.merge!(e.to_ast)
7
+ ast
8
+ else
9
+ case a.text_value
10
+
11
+ when 'first-child'
12
+ { "pseudo_function" => 'nth-child', 'a' => 0, 'b' => 1 }
13
+
14
+ when 'last-child'
15
+ { "pseudo_function" => 'nth-last-child', 'a' => 0, 'b' => 1 }
16
+
17
+ else
18
+ { "pseudo_class" => a.text_value }
19
+
20
+ end
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,17 @@
1
+ module JSONSelect::Ast::SelectorGroup
2
+
3
+ def to_ast
4
+ if b.elements.empty?
5
+ return a.to_ast
6
+ end
7
+
8
+ ast = [',', a.to_ast]
9
+
10
+ b.elements.each do |group|
11
+ ast.push group.c.to_ast
12
+ end
13
+
14
+ ast
15
+ end
16
+
17
+ end
@@ -0,0 +1,7 @@
1
+ module JSONSelect::Ast::SimpleExpr
2
+
3
+ def to_ast
4
+ { 'a' => 0, 'b' => text_value.to_i }
5
+ end
6
+
7
+ end
@@ -0,0 +1,23 @@
1
+ module JSONSelect::Ast::SimpleSelector
2
+
3
+ def to_ast
4
+ ast = {}
5
+
6
+ if respond_to?(:a) and respond_to?(:b)
7
+ ast.merge! a.to_ast
8
+
9
+ b.elements.each do |s|
10
+ ast.merge!(s.to_ast)
11
+ end
12
+
13
+ else
14
+ self.elements.each do |s|
15
+ ast.merge!(s.to_ast)
16
+ end
17
+
18
+ end
19
+
20
+ ast
21
+ end
22
+
23
+ end
@@ -0,0 +1,8 @@
1
+ module JSONSelect::Ast::TypeSelector
2
+
3
+ # `object` | `array` | `number` | `string` | `boolean` | `null`
4
+ def to_ast
5
+ { "type" => self.text_value }
6
+ end
7
+
8
+ end
@@ -0,0 +1,7 @@
1
+ module JSONSelect::Ast::UniversalSelector
2
+
3
+ def to_ast
4
+ {}
5
+ end
6
+
7
+ end
@@ -0,0 +1,19 @@
1
+ class JSONSelect::Parser
2
+
3
+ def initialize(source)
4
+ @parser = JSONSelect::SelectorParserParser.new
5
+ @source = source
6
+ end
7
+
8
+ def parse
9
+ tree = @parser.parse(@source)
10
+ if tree
11
+ JSONSelect::Selector.new(tree.to_ast)
12
+ else
13
+ raise JSONSelect::ParseError, @parser.failure_reason
14
+ # puts parser.failure_line
15
+ # puts parser.failure_column
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,176 @@
1
+ class JSONSelect::Selector
2
+
3
+ attr_reader :ast
4
+
5
+ def initialize(ast)
6
+ @ast = ast
7
+ end
8
+
9
+ def match(object)
10
+ matches = []
11
+
12
+ _each(@ast, object, nil, nil, nil) do |object|
13
+ matches << object
14
+ end
15
+
16
+ matches
17
+ end
18
+
19
+ alias_method :evaluate, :match
20
+ alias_method :=~, :match
21
+ alias_method :===, :match
22
+
23
+ private
24
+
25
+ # function forEach(sel, obj, fun, id, num, tot) {
26
+ # var a = (sel[0] === ',') ? sel.slice(1) : [sel];
27
+ # var a0 = [];
28
+ # var call = false;
29
+ # for (var i = 0; i < a.length; i++) {
30
+ # var x = mn(obj, a[i], id, num, tot);
31
+ # if (x[0]) call = true;
32
+ # for (var j = 0; j < x[1].length; j++) a0.push(x[1][j]);
33
+ # }
34
+ # if (a0.length && typeof obj === 'object') {
35
+ # if (a0.length >= 1) a0.unshift(",");
36
+ # if (isArray(obj)) {
37
+ # for (var i = 0; i < obj.length; i++) forEach(a0, obj[i], fun, undefined, i, obj.length);
38
+ # } else {
39
+ # // it's a shame to do this for :last-child and other
40
+ # // properties which count from the end when we don't
41
+ # // even know if they're present. Also, the stream
42
+ # // parser is going to be pissed.
43
+ # var l = 0;
44
+ # for (var k in obj) if (obj.hasOwnProperty(k)) l++;
45
+ # var i = 0;
46
+ # for (var k in obj) if (obj.hasOwnProperty(k)) forEach(a0, obj[k], fun, k, i++, l);
47
+ # }
48
+ # }
49
+ # if (call && fun) fun(obj);
50
+ # };
51
+ def _each(selector, object, id, number, total, &block)
52
+ a0 = (selector[0] == ',' ? selector[1..-1] : [selector])
53
+ a1 = []
54
+
55
+ call = false
56
+
57
+ a0.each do |selector|
58
+ ok, extra = _match(object, selector, id, number, total)
59
+
60
+ call = true if ok
61
+ a1.concat extra
62
+ end
63
+
64
+ if (Array === object or Hash === object) and a1.any?
65
+ a1.unshift(',')
66
+ size = object.size
67
+
68
+ if Array === object
69
+ object.each_with_index do |child, idx|
70
+ _each(a1, child, nil, idx, size, &block)
71
+ end
72
+ end
73
+
74
+ if Hash === object
75
+ object.each_with_index do |(key, child), idx|
76
+ _each(a1, child, key, idx, size, &block)
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ if call and block
83
+ block.call(object)
84
+ end
85
+ end
86
+
87
+
88
+ # function mn(node, sel, id, num, tot) {
89
+ # var sels = [];
90
+ # var cs = (sel[0] === '>') ? sel[1] : sel[0];
91
+ # var m = true;
92
+ # if (cs.type) m = m && (cs.type === mytypeof(node));
93
+ # if (cs.id) m = m && (cs.id === id);
94
+ # if (m && cs.pf) {
95
+ # if (cs.pf === ":nth-last-child") num = tot - num;
96
+ # else num++;
97
+ # if (cs.a === 0) {
98
+ # m = cs.b === num;
99
+ # } else {
100
+ # m = (!((num - cs.b) % cs.a) && ((num*cs.a + cs.b) >= 0));
101
+ # }
102
+ # }
103
+ #
104
+ # // should we repeat this selector for descendants?
105
+ # if (sel[0] !== '>' && sel[0].pc !== ":root") sels.push(sel);
106
+ #
107
+ # if (m) {
108
+ # // is there a fragment that we should pass down?
109
+ # if (sel[0] === '>') { if (sel.length > 2) { m = false; sels.push(sel.slice(2)); } }
110
+ # else if (sel.length > 1) { m = false; sels.push(sel.slice(1)); }
111
+ # }
112
+ #
113
+ # return [m, sels];
114
+ # }
115
+ def _match(object, selector, id, number, total)
116
+ selectors = []
117
+ current_selector = (selector[0] == '>' ? selector[1] : selector[0])
118
+ match = true
119
+
120
+ if current_selector.key?('type')
121
+ match = (match and current_selector['type'] == _type_of(object))
122
+ end
123
+
124
+ if current_selector.key?('class')
125
+ match = (match and current_selector['class'] == id)
126
+ end
127
+
128
+ if match and current_selector.key?('pseudo_function')
129
+ pseudo_function = current_selector['pseudo_function']
130
+
131
+ if pseudo_function == 'nth-last-child'
132
+ number = total - number
133
+ else
134
+ number += 1
135
+ end
136
+
137
+ if current_selector['a'] == 0
138
+ match = current_selector['b'] == number
139
+ else
140
+ # WTF!
141
+ match = ((((number - current_selector['b']) % current_selector['a']) == 0) && ((number * current_selector['a'] + current_selector['b']) >= 0))
142
+ end
143
+ end
144
+
145
+ if selector[0] != '>' and selector[0]['pseudo_class'] != 'root'
146
+ selectors.push selector
147
+ end
148
+
149
+ if match
150
+ if selector[0] == '>'
151
+ if selector.length > 2
152
+ m = false
153
+ selectors.push selector[2..-1]
154
+ end
155
+ elsif selector.length > 1
156
+ m = false
157
+ selectors.push selector[1..-1]
158
+ end
159
+ end
160
+
161
+ return [match, selectors]
162
+ end
163
+
164
+ def _type_of(object)
165
+ case object
166
+ when Hash then 'object'
167
+ when Array then 'array'
168
+ when String then 'string'
169
+ when Numeric then 'number'
170
+ when TrueClass then 'boolean'
171
+ when FalseClass then 'boolean'
172
+ when NilClass then 'null'
173
+ end
174
+ end
175
+
176
+ end