pebbles-uid 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pebbles-uid.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Katrina Owen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # Pebbles::Uid
2
+
3
+ Handle unique identifiers in the Pebblestack universe conveniently.
4
+
5
+ ## Unique Identifiers
6
+
7
+ A valid Uid is in the format `genus[.species]:path[$oid]`.
8
+
9
+ ### Genus
10
+
11
+ The genus is one or more dot-delimited labels.
12
+
13
+ The first label uniquely identifies which pebble the resource originated from.
14
+
15
+ Any secondary labels are grouped into a `species`, which identify sub-types within a given pebble.
16
+
17
+ ```
18
+ post.doc
19
+ ```
20
+
21
+ Here the genus is `post`, and the species is `doc`.
22
+
23
+ ### Path
24
+
25
+ The path is a dot-delimited set of labels. The first label represents the realm, and is required.
26
+
27
+ A single resource may have multiple paths (and therefore multiple uids). For example:
28
+
29
+ ```
30
+ post.card:tourism.norway.fjords$42
31
+ post.card:tourism.norway.west-coast$42
32
+ ```
33
+
34
+ ### Oid
35
+
36
+ The object identifier can be nearly any string, including another uid.
37
+
38
+ The oid _MUST_ be unique for any genus and realm.
39
+
40
+ There are currently two characters which cannot be included in an oid:
41
+
42
+ `|` (pipe) and `,` (comma).
43
+
44
+ Oid is optional when creating a new resource.
45
+
46
+ ## Queries
47
+
48
+ ### For one specific resource
49
+
50
+ Across the pebblestack universe, you can query for a specific resource by passing a full Uid.
51
+
52
+ In addition, the wildcard query `genus:realm.*$oid` will identify a single resource, e.g.:
53
+
54
+ ```
55
+ post.card:tourism.*$42
56
+ ```
57
+
58
+ If a specific resource is targeted (i.e. genus and oid are both unambiguous), then realm is required.
59
+
60
+ ### For a collection of resources
61
+
62
+ #### Asterisks
63
+
64
+ The most permissive query consists of any genus in any path, with any oid (implied):
65
+
66
+ ```
67
+ *:*
68
+ ```
69
+
70
+ This is the equivalent to:
71
+
72
+ ```
73
+ *:*$*
74
+ ```
75
+
76
+ Queries may not be made across multiple realms at one time. If realm is not specified in the path, then it is assumed that the application verifies realm if necessary.
77
+
78
+ In the context of a genus or a path, you may specify any number of labels and terminate with an asterisk, which means: zero or more labels follow. E.g.:
79
+
80
+ ```
81
+ post.*:tourism.europe.*
82
+ ```
83
+
84
+ NOTE: wildcard queries on species is not yet supported.
85
+
86
+ This will return things like:
87
+
88
+ ```
89
+ post:tourism.europe$123
90
+ post:tourism.europe.france.sightseeing$234
91
+ post.card:tourism.europe.mountains$345
92
+ ```
93
+
94
+ Asterisks may not occur in mid-sequence. In other words, `post:tourism.*.food` will not be accepted.
95
+
96
+ In the context of an oid, you either know the oid you want, or you don't. A query where oid is not specified at all is equivalent to an oid represented by an asterisk.
97
+
98
+ #### Pipes
99
+
100
+ A pipe signifies _or_.
101
+
102
+ This can be used in genus at any position:
103
+
104
+ ```
105
+ unit|group:*
106
+ ```
107
+
108
+ ```
109
+ post.doc|card:*
110
+ ```
111
+
112
+ In a path, this can be used in any position except the first, as the realm must be unambiguous.
113
+
114
+ Thus:
115
+
116
+ ```
117
+ post:realm.label1|label2.something
118
+ ```
119
+
120
+ The following is not allowed:
121
+
122
+ ```
123
+ post:realm1|realm2.*
124
+ ```
125
+
126
+ If you wish to fetch a specific list of objects, you may use the pipe to delimit oids:
127
+
128
+ ```
129
+ post:realm.*$a|b|c|d|e|f|g
130
+ ```
131
+
132
+ This is equivalent to the NOW deprecated comma-delimited uid query:
133
+
134
+ ```
135
+ post:realm.*$a,post:realm.*$b,post:realm.*$c,post:realm.*$d,post:realm.*$e,post:realm.*$f,post:realm.*$g,
136
+ ```
137
+
138
+ NOTE: realm is required in this case, because each term in the list must refer to a single resource.
139
+
140
+ #### Caret
141
+
142
+ In both genus and paths, a caret indicates ancestry up to the specified point:
143
+
144
+ ```
145
+ post:realm.europe.^norway.fjords.food
146
+ ```
147
+
148
+ This represents everything at the following locations:
149
+
150
+ ```
151
+ post:realm.europe
152
+ post:realm.europe.norway
153
+ post:realm.europe.norway.fjords
154
+ post:realm.europe.norway.fjords.food
155
+ ```
156
+
157
+
158
+ ## Installation
159
+
160
+ Add this line to your application's Gemfile:
161
+
162
+ gem 'pebbles-uid'
163
+
164
+ And then execute:
165
+
166
+ $ bundle
167
+
168
+ Or install it yourself as:
169
+
170
+ $ gem install pebbles-uid
171
+
172
+ ## Usage
173
+
174
+ ### Identifying an existing resource
175
+
176
+ uid = Pebbles::Uid.new('post.card:tourism.norway.fjords$1234')
177
+
178
+ uid.realm
179
+ => "tourism"
180
+
181
+ uid.genus
182
+ => "post.card"
183
+
184
+ uid.species
185
+ => "card"
186
+
187
+ uid.path
188
+ => "tourism.norway.fjords"
189
+
190
+ uid.oid
191
+ => 1234
192
+
193
+ uid.to_s
194
+ => 'post.card:tourism.norway.fjords$1234'
195
+
196
+ uid.to_hash
197
+ => {'genus' => 'post.card', 'path_0' => 'tourism', 'path_1' => 'norway', 'path_2' => 'fjords', 'oid' => '1234'}
198
+
199
+ uid.to_hash(:verbose => true, :suffix => '')
200
+ => {'genus_0_' => 'post', 'genus_1_' => 'card', 'path_0_' => 'tourism', 'path_1_' => 'norway', 'path_2_' => 'fjords', 'oid_' => '1234'}
201
+
202
+ uid.to_hash(:verbose => true, :suffix => 'xyz', :genus => 'klass', :path => 'label' => :oid => 'id')
203
+ => {'klass_0_xyz' => 'post', 'klass_1_xyz' => 'card', 'label_0_xyz' => 'tourism', 'label_1_xyz' => 'norway', 'label_2_xyz' => 'fjords', 'id_xyz' => '1234'}
204
+
205
+ ## Contributing
206
+
207
+ 1. Fork it
208
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
209
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
210
+ 4. Push to the branch (`git push origin my-new-feature`)
211
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,129 @@
1
+ require "pebbles-uid/version"
2
+
3
+ require 'pebbles-uid/query'
4
+ require "pebbles-uid/conditions"
5
+ require "pebbles-uid/labels"
6
+ require "pebbles-uid/genus"
7
+ require "pebbles-uid/path"
8
+ require "pebbles-uid/oid"
9
+
10
+ module Pebbles
11
+ class Uid
12
+
13
+ class << self
14
+
15
+ def query(s)
16
+ Pebbles::Uid::Query.new(s)
17
+ end
18
+
19
+ def parse(s)
20
+ /^(?<genus>.*):(?<path>[^\$]*)\$?(?<oid>.*)$/ =~ s
21
+ [genus, path, oid.empty? ? nil : oid]
22
+ end
23
+
24
+ def genus(s)
25
+ parse(s)[0]
26
+ end
27
+
28
+ def path(s)
29
+ parse(s)[1]
30
+ end
31
+
32
+ def oid(s)
33
+ parse(s)[2]
34
+ end
35
+
36
+ def valid_path?(path)
37
+ return false if path.empty?
38
+ Path.new(path).valid_with?(/^[a-z0-9_-]+$/)
39
+ end
40
+
41
+ def valid_genus?(genus)
42
+ return false if genus.empty?
43
+ Genus.new(genus).valid_with?(/^[a-z0-9_-]+$/)
44
+ end
45
+
46
+ def valid_oid?(oid)
47
+ return true if !oid || oid.empty?
48
+ !!(oid =~ /^[^,|]+$/)
49
+ end
50
+
51
+ def cache_key(uid)
52
+ genus, path, oid = parse(uid)
53
+ "#{genus}:#{Path.new(path).realm}.*$#{oid}"
54
+ end
55
+ end
56
+
57
+ attr_reader :genus, :path, :oid
58
+ def initialize(s)
59
+ @genus, @path, @oid = self.class.parse(s)
60
+ unless valid_genus?
61
+ raise ArgumentError.new("Invalid Uid: #{s}. Genus is invalid.")
62
+ end
63
+ unless valid_path?
64
+ raise ArgumentError.new("Invalid Uid: #{s}. Path is invalid.")
65
+ end
66
+
67
+ if oid == '*'
68
+ raise ArgumentError.new("Invalid Uid: #{s}. Cannot have wildcard oid.")
69
+ end
70
+
71
+ if genus.empty?
72
+ raise ArgumentError.new("Invalid Uid: #{s}. Genus is required.")
73
+ end
74
+ if path.empty?
75
+ raise ArgumentError.new("Invalid Uid: #{s}. Realm is required.")
76
+ end
77
+ end
78
+
79
+ def realm
80
+ @realm ||= path.split('.').first
81
+ end
82
+
83
+ def species
84
+ @species ||= genus_labels.species
85
+ end
86
+
87
+ def path_labels
88
+ @path_labels ||= Path.new(path)
89
+ end
90
+
91
+ def genus_labels
92
+ @genus_labels ||= Genus.new(genus)
93
+ end
94
+
95
+ def valid?
96
+ valid_genus? && valid_path? && valid_oid?
97
+ end
98
+
99
+ def valid_genus?
100
+ self.class.valid_genus?(genus)
101
+ end
102
+
103
+ def valid_path?
104
+ self.class.valid_path?(path)
105
+ end
106
+
107
+ def valid_oid?
108
+ self.class.valid_oid?(oid)
109
+ end
110
+
111
+ def oid?
112
+ !!oid
113
+ end
114
+
115
+ def to_s
116
+ s = "#{genus}:#{path}"
117
+ s << "$#{oid}" if oid
118
+ s
119
+ end
120
+
121
+ def parsed
122
+ [genus, path, oid].compact
123
+ end
124
+
125
+ def cache_key
126
+ "#{genus}:#{realm}.*$#{oid}"
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,73 @@
1
+ # A key => value representation of sets of labels
2
+ # used for elastic search or active record as indexing
3
+ # or search conditions.
4
+ module Pebbles
5
+ class Uid
6
+ class Conditions
7
+
8
+ NO_MARKER = Class.new
9
+
10
+ attr_reader :values, :name, :suffix, :stop, :verbose
11
+
12
+ def initialize(values, options = {})
13
+ @values = values
14
+ @name = options.fetch(:name) { 'label' }
15
+ @suffix = options.fetch(:suffix) { nil }
16
+ @verbose = options.fetch(:verbose) { true }
17
+ @stop = options.fetch(:stop) { NO_MARKER }
18
+ if values.last == '*'
19
+ @stop = NO_MARKER
20
+ @values.pop
21
+ end
22
+ end
23
+
24
+ alias :verbose? :verbose
25
+
26
+ def next
27
+ label(values.length)
28
+ end
29
+
30
+ def to_hash
31
+ return {label => values.join('.')} unless verbose?
32
+
33
+ collection = labelize
34
+ collection.merge!(stop_label) if use_stop_marker?
35
+ collection
36
+ end
37
+
38
+ def labelize
39
+
40
+ optional_part = false
41
+ labels = values.map do |label|
42
+ if label =~ /^\^/
43
+ label.gsub!(/^\^/, '')
44
+ optional_part = true
45
+ end
46
+
47
+ result = label.include?('|') ? label.split('|') : label
48
+ result = [result, nil].flatten if optional_part
49
+ result
50
+ end
51
+
52
+ collection = {}
53
+ labels.each_with_index do |value, i|
54
+ collection[label(i)] = value
55
+ end
56
+ collection
57
+ end
58
+
59
+ def label(i = nil)
60
+ [name, i, suffix].compact.join('_')
61
+ end
62
+
63
+ def stop_label
64
+ { label(values.length) => stop }
65
+ end
66
+
67
+ def use_stop_marker?
68
+ stop != NO_MARKER
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ module Pebbles
2
+ class Uid
3
+ class Genus < Labels
4
+
5
+ def ambiguous?
6
+ value == '*' || values.empty? || wildcard?
7
+ end
8
+
9
+ def species
10
+ return if size <= 1
11
+
12
+ tail.join('.')
13
+ end
14
+
15
+ def species?
16
+ species != '*'
17
+ end
18
+
19
+ def to_hash(options = {})
20
+ super({:verbose => false, :name => 'genus'}.merge(options))
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ module Pebbles
2
+ class Uid
3
+ class Labels
4
+
5
+ attr_reader :values, :prefix
6
+ def initialize(*values)
7
+ @values = values.flatten.compact.map {|v| v.split('.') }.flatten
8
+ end
9
+
10
+ def tail
11
+ values[1..-1]
12
+ end
13
+
14
+ def valid_with?(pattern)
15
+ !empty? && values.all? {|value| value[pattern] }
16
+ end
17
+
18
+ def size
19
+ values.size
20
+ end
21
+
22
+ def to_a
23
+ values
24
+ end
25
+
26
+ def to_s
27
+ values.join('.')
28
+ end
29
+ alias :value :to_s
30
+
31
+ def empty?
32
+ values.empty?
33
+ end
34
+
35
+ def wildcard?
36
+ !!(value =~ /[\*\|\^]/)
37
+ end
38
+
39
+ def to_hash(options = {})
40
+ Conditions.new(values, options).to_hash
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ module Pebbles
2
+ class Uid
3
+
4
+ class Oid < Labels
5
+
6
+ def ambiguous?
7
+ value == '*' || value.empty?
8
+ end
9
+
10
+ def empty?
11
+ value.empty?
12
+ end
13
+
14
+ def multiple?
15
+ !!(value =~ /[\|]/)
16
+ end
17
+
18
+ def wildcard?
19
+ value == '*' || multiple?
20
+ end
21
+
22
+ def to_hash(options = {})
23
+ options.delete(:verbose)
24
+ super({:verbose => false, :name => 'oid'}.merge(options))
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ module Pebbles
2
+ class Uid
3
+ class Path < Labels
4
+
5
+ def realm
6
+ values.first
7
+ end
8
+
9
+ def realm?
10
+ return false if realm == '*'
11
+ !!realm
12
+ end
13
+
14
+ def to_hash(options = {})
15
+ super({:name => 'path'}.merge(options))
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,89 @@
1
+ module Pebbles
2
+ class Uid
3
+ class Query
4
+
5
+ attr_reader :term, :terms, :genus, :path, :oid
6
+ def initialize(term)
7
+ @term = term
8
+
9
+ if wildcard_query?
10
+ @terms = [term]
11
+ @genus, @path, @oid = Pebbles::Uid.parse(term)
12
+ else
13
+ @terms = extract_terms
14
+ end
15
+ end
16
+
17
+ def for_one?
18
+ !wildcard_query? && terms.size == 1
19
+ end
20
+
21
+ def list?
22
+ terms.size != 1
23
+ end
24
+
25
+ def collection?
26
+ wildcard_query?
27
+ end
28
+
29
+ def path?
30
+ @path && @path.split('.').reject {|s| s == '*'}.length > 0
31
+ end
32
+
33
+ def genus?
34
+ @genus && @genus != '*'
35
+ end
36
+
37
+ def species?
38
+ !!Genus.new(genus).species
39
+ end
40
+
41
+ def species
42
+ Genus.new(genus).species
43
+ end
44
+
45
+ def oid?
46
+ !!@oid && @oid != '*'
47
+ end
48
+
49
+ def cache_keys
50
+ terms.map { |t| Pebbles::Uid.cache_key(t) }
51
+ end
52
+
53
+ private
54
+
55
+ def wildcard_query?
56
+ return false if term.include?(',')
57
+ species, _, oid = Pebbles::Uid.parse(term)
58
+ return true if Genus.new(species).wildcard?
59
+ return true if oid.nil? || oid.empty? || oid == '*'
60
+ false
61
+ end
62
+
63
+ def extract_terms
64
+ term.split(',').map do |uid|
65
+ species, path, oid = Pebbles::Uid.parse(uid)
66
+ species_labels = Genus.new(species)
67
+ path_labels = Path.new(path)
68
+ oid_box = Oid.new(oid)
69
+
70
+ raise ArgumentError.new('Realm must be specified') unless path_labels.realm?
71
+ raise ArgumentError.new('Genus must unambiguous') if species_labels.ambiguous?
72
+ raise ArgumentError.new('Oid must be unambiguous') if oid_box.ambiguous?
73
+
74
+ @realm ||= path_labels.realm
75
+ raise ArgumentError.new('One realm at a time, please') if @realm != path_labels.realm
76
+
77
+ if oid_box.multiple?
78
+ oid.split('|').map do |s|
79
+ "#{species}:#{path}$#{s}"
80
+ end
81
+ else
82
+ uid
83
+ end
84
+ end.flatten
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ module Pebbles
2
+ class Uid
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ module Pebbles
2
+ class Uid
3
+ module Wildcard
4
+
5
+ class << self
6
+
7
+ def valid?(path)
8
+ stars_are_solitary?(path) && single_caret?(path) && pipes_are_interleaved?(path) && carets_are_leading?(path) && stars_are_terminating?(path)
9
+ end
10
+
11
+ # a.*.c passes
12
+ # a.*b.c does not
13
+ # A later rule ensures that * always falls at the end of a path
14
+ def stars_are_solitary?(path)
15
+ !path.split('.').any? {|s| s.match(/.+\*|\*.+/)}
16
+ end
17
+
18
+ # a.b|c.d passes
19
+ # a.|b.c does not
20
+ def pipes_are_interleaved?(path)
21
+ !path.split('.').any? {|s| s.match(/^\||\|$/)}
22
+ end
23
+
24
+ # a.^b.c passes
25
+ # a.^c.^d does not
26
+ def single_caret?(path)
27
+ path.split('.').select {|s| s.match(/\^/) }.size <= 1
28
+ end
29
+
30
+ # a.^b.c passes
31
+ # a.b^c.d does not
32
+ def carets_are_leading?(path)
33
+ !path.split('.').any? {|s| s.match(/.+\^|\^$/) }
34
+ end
35
+
36
+ # a.b.* passes
37
+ # *.b.c does not
38
+ def stars_are_terminating?(path)
39
+ path !~ /.*\*\.\w/
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1 @@
1
+ require 'pebbles-uid'
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/pebbles-uid/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Katrina Owen"]
6
+ gem.email = ["katrina.owen@gmail.com"]
7
+ gem.description = %q{Handle pebble UIDs conveniently.}
8
+ gem.summary = %q{Unique identifiers in the Pebblestack universe, in the format species[.genus]:path$oid, where the path is a dot-delimited set of labels, the first of which (realm) is required.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "pebbles-uid"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Pebbles::Uid::VERSION
17
+
18
+ gem.add_development_dependency "rspec"
19
+ end
@@ -0,0 +1,55 @@
1
+ require 'pebbles-uid/conditions'
2
+
3
+ describe Pebbles::Uid::Conditions do
4
+
5
+ subject { Pebbles::Uid::Conditions.new(%w(a b c)) }
6
+
7
+ its(:to_hash) { should eq('label_0' => "a", 'label_1' => "b", 'label_2' => "c") }
8
+
9
+ describe "customized labels" do
10
+ subject { Pebbles::Uid::Conditions.new(%w(p r q), :name => 'dot', :suffix => '') }
11
+ its(:to_hash) { should eq('dot_0_' => 'p', 'dot_1_' => 'r', 'dot_2_' => 'q') }
12
+ end
13
+
14
+ it "has a non-verbose mode" do
15
+ uid = Pebbles::Uid::Conditions.new(%w(k l m), :name => 'stuff', :verbose => false)
16
+ uid.to_hash.should eq({'stuff' => 'k.l.m'})
17
+ end
18
+
19
+ specify "non-verbose still takes a suffix" do
20
+ uid = Pebbles::Uid::Conditions.new(%w(k l m), :name => 'stuff', :verbose => false, :suffix => 'xyz')
21
+ uid.to_hash.should eq({'stuff_xyz' => 'k.l.m'})
22
+ end
23
+
24
+ describe "with a stop label" do
25
+ subject { Pebbles::Uid::Conditions.new(%w(x y z), :stop => nil) }
26
+ its(:to_hash) { should eq('label_0' => "x", 'label_1' => "y", 'label_2' => "z", 'label_3' => nil) }
27
+ end
28
+
29
+ describe "next label" do
30
+ subject { Pebbles::Uid::Conditions.new(%w(h j k l), :name => 'vim') }
31
+ its(:next) { should eq('vim_4') }
32
+ end
33
+
34
+ describe "with pipes" do
35
+ subject { Pebbles::Uid::Conditions.new(%w(a b|c d), :stop => nil) }
36
+ its(:to_hash) { should eq('label_0' => "a", 'label_1' => ['b', 'c'], 'label_2' => "d", 'label_3' => nil) }
37
+ end
38
+
39
+ describe "with an asterisk" do
40
+ # ignores stop marker if it is terminated by an asterisk
41
+ subject { Pebbles::Uid::Conditions.new(%w(a b *), :stop => nil) }
42
+ its(:to_hash) { should eq('label_0' => "a", 'label_1' => 'b') }
43
+ end
44
+
45
+ describe "with an asterisk" do
46
+ subject { Pebbles::Uid::Conditions.new(%w(a ^b c), :stop => nil) }
47
+ its(:to_hash) { should eq('label_0' => "a", 'label_1' => ['b', nil], 'label_2' => ['c', nil], 'label_3' => nil) }
48
+ end
49
+
50
+ describe "complicated stuff" do
51
+ subject { Pebbles::Uid::Conditions.new(%w(a ^b|c d *), :stop => nil) }
52
+ its(:to_hash) { should eq('label_0' => "a", 'label_1' => ['b', 'c', nil], 'label_2' => ['d', nil]) }
53
+ end
54
+
55
+ end
@@ -0,0 +1,33 @@
1
+ require 'pebbles-uid/labels'
2
+ require 'pebbles-uid/conditions'
3
+ require 'pebbles-uid/genus'
4
+
5
+ describe Pebbles::Uid::Genus do
6
+
7
+ subject { Pebbles::Uid::Genus.new('unicorn') }
8
+
9
+ its(:to_s) { should eq('unicorn') }
10
+
11
+ its(:species) { should be_nil }
12
+ its(:to_hash) { should eq('genus' => 'unicorn') }
13
+
14
+ context "subtypes" do
15
+
16
+ subject { Pebbles::Uid::Genus.new('unicorn', 'dust', 'sparkles') }
17
+ its(:to_s) { should eq('unicorn.dust.sparkles') }
18
+ its(:species) { should eq('dust.sparkles') }
19
+ its(:to_hash) { should eq('genus' => 'unicorn.dust.sparkles') }
20
+
21
+ it "doesn't have a species if there's a wildcard" do
22
+ genus = Pebbles::Uid::Genus.new('unicorn.*')
23
+ genus.species?.should == false
24
+ end
25
+
26
+ it "can customize the hash" do
27
+ subject.to_hash(:verbose => true).should eq('genus_0' => 'unicorn', 'genus_1' => 'dust', 'genus_2' => 'sparkles')
28
+ end
29
+ end
30
+
31
+ it { Pebbles::Uid::Genus.new('unicorn.horn').species.should eq('horn') }
32
+
33
+ end
@@ -0,0 +1,37 @@
1
+ require 'pebbles-uid/conditions'
2
+ require 'pebbles-uid/labels'
3
+
4
+ describe Pebbles::Uid::Labels do
5
+
6
+ subject { Pebbles::Uid::Labels.new('a.b.c') }
7
+ its(:to_s) { should eq('a.b.c') }
8
+ its(:to_a) { should eq(%w(a b c)) }
9
+ its(:to_hash) { should eq('label_0' => "a", 'label_1' => "b", 'label_2' => "c") }
10
+ its(:tail) { should eq(%w(b c)) }
11
+
12
+ it "delegates to conditions" do
13
+ subject.to_hash(:name => 'thing', :verbose => false).should eq({'thing' => 'a.b.c'})
14
+ end
15
+
16
+ it "has a size" do
17
+ subject.size.should eq(3)
18
+ end
19
+
20
+ describe "alternate constructors" do
21
+ it { Pebbles::Uid::Labels.new('a', 'b', 'c').to_s.should eq('a.b.c') }
22
+ it { Pebbles::Uid::Labels.new(['a', 'b', 'c']).to_s.should eq('a.b.c') }
23
+ end
24
+
25
+ # Not certain where I want to deal with invalid wildcard paths yet.
26
+ context "wildcards" do
27
+ specify { Pebbles::Uid::Labels.new('a.*').wildcard?.should == true }
28
+ specify { Pebbles::Uid::Labels.new('a.b|c').wildcard?.should == true }
29
+ specify { Pebbles::Uid::Labels.new('a.^b.c').wildcard?.should == true }
30
+ end
31
+
32
+ context "validation" do
33
+ it { subject.valid_with?(/[0-9]/).should == false }
34
+ it { subject.valid_with?(/[a-z]/).should == true }
35
+ end
36
+
37
+ end
data/spec/oid_spec.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'pebbles-uid/labels'
2
+ require 'pebbles-uid/oid'
3
+
4
+ describe Pebbles::Uid::Oid do
5
+
6
+ describe "wildcard" do
7
+ specify { Pebbles::Uid::Oid.new('*').wildcard?.should == true }
8
+ specify { Pebbles::Uid::Oid.new('star*star').wildcard?.should == false }
9
+ specify { Pebbles::Uid::Oid.new('abc|xyz').wildcard?.should == true }
10
+ end
11
+
12
+ end
data/spec/path_spec.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'pebbles-uid/conditions'
2
+ require 'pebbles-uid/labels'
3
+ require 'pebbles-uid/path'
4
+
5
+ describe Pebbles::Uid::Path do
6
+
7
+ subject { Pebbles::Uid::Path.new('magical', 'forrest', 'clearing') }
8
+
9
+ its(:realm) { should eq('magical') }
10
+ its(:realm?) { should == true }
11
+ its(:to_s) { should eq('magical.forrest.clearing') }
12
+ its(:to_a) { should eq(%w(magical forrest clearing)) }
13
+ its(:to_hash) { should eq({'path_0' => 'magical', 'path_1' => 'forrest', 'path_2' => 'clearing'}) }
14
+ its(:wildcard?) { should == false }
15
+
16
+ it "can customize the hash" do
17
+ subject.to_hash(:name => 'label').should eq({'label_0' => 'magical', 'label_1' => 'forrest', 'label_2' => 'clearing'})
18
+ end
19
+
20
+ specify "wildcard realm" do
21
+ Pebbles::Uid::Path.new('*').realm?.should == false
22
+ end
23
+
24
+ end
@@ -0,0 +1,128 @@
1
+ require 'pebbles-uid'
2
+
3
+ describe Pebbles::Uid::Query do
4
+
5
+ context "for a single resource" do
6
+ let(:term) { "post:area51$abc" }
7
+ subject { Pebbles::Uid::Query.new(term) }
8
+
9
+ its(:term) { should == term }
10
+
11
+ its(:for_one?) { should == true }
12
+ its(:list?) { should == false }
13
+ its(:collection?) { should == false }
14
+ its(:cache_keys) { should eq(['post:area51.*$abc']) }
15
+
16
+ it "handles a wildcard path if realm is given" do
17
+ query = Pebbles::Uid::Query.new('post:area51.*$abc')
18
+ query.for_one?.should == true
19
+ end
20
+
21
+ it "ignores crazy wildcard stuff in the path" do
22
+ query = Pebbles::Uid::Query.new("post:area51.^a.b.c.*|d$abc")
23
+ query.for_one?.should == true
24
+ end
25
+
26
+ it "bails without realm" do
27
+ ->{ Pebbles::Uid::Query.new('post:*$abc') }.should raise_error(ArgumentError)
28
+ ->{ Pebbles::Uid::Query.new('post:$abc') }.should raise_error(ArgumentError)
29
+ end
30
+ end
31
+
32
+ context "for a list of resources" do
33
+ let(:term) { "post:area51$abc,post:area51$xyz" }
34
+ subject { Pebbles::Uid::Query.new(term) }
35
+
36
+ its(:for_one?) { should == false }
37
+ its(:list?) { should == true }
38
+ its(:collection?) { should == false }
39
+
40
+ its(:cache_keys) { should eq(['post:area51.*$abc', 'post:area51.*$xyz']) }
41
+
42
+ it "handles a pipe-delimited list of oids" do
43
+ query = Pebbles::Uid::Query.new("post:area51$abc|xyz")
44
+ query.terms.should eq(['post:area51$abc', 'post:area51$xyz'])
45
+ end
46
+
47
+ it "ignores crazy wildcard stuff in the path" do
48
+ query = Pebbles::Uid::Query.new("post:area51.^a.b.c.*|d$abc|xyz")
49
+ query.list?.should == true
50
+ query.cache_keys.should eq(['post:area51.*$abc', 'post:area51.*$xyz'])
51
+ end
52
+
53
+ it "bails without realm" do
54
+ ->{ Pebbles::Uid::Query.new('post:*$abc,post:*$xyz') }.should raise_error(ArgumentError)
55
+ end
56
+
57
+ it "must have only one realm" do
58
+ ->{ Pebbles::Uid::Query.new('post:area51$abc,post:area52$xyz') }.should raise_error(ArgumentError)
59
+ end
60
+
61
+ it "bails with unspecific genus" do
62
+ ->{ Pebbles::Uid::Query.new('post.*:area51$abc,post:area51$xyz') }.should raise_error(ArgumentError)
63
+ end
64
+
65
+ it "bails with unspecified oid" do
66
+ ->{ Pebbles::Uid::Query.new('post:area51$*,post:area51$xyz') }.should raise_error(ArgumentError)
67
+ end
68
+ end
69
+
70
+ context "for a collection of resources" do
71
+ let(:term) { "post.*:area51.^a.b.c" }
72
+ subject { Pebbles::Uid::Query.new(term) }
73
+
74
+ its(:for_one?) { should == false }
75
+ its(:list?) { should == false }
76
+ its(:collection?) { should == true }
77
+
78
+ it "handles unspecific oids" do
79
+ Pebbles::Uid::Query.new('post:area51$*').collection?.should == true
80
+ Pebbles::Uid::Query.new('post:area51').collection?.should == true
81
+ end
82
+ end
83
+
84
+ describe "wildcard queries" do
85
+
86
+ context "everything" do
87
+ subject { Pebbles::Uid::Query.new('*:*') }
88
+
89
+ its(:genus?) { should == false }
90
+ its(:path?) { should == false }
91
+ its(:oid?) { should == false }
92
+ end
93
+
94
+ context "everything, with any oid" do
95
+ subject { Pebbles::Uid::Query.new('*:*$*') }
96
+
97
+ its(:genus?) { should == false }
98
+ its(:path?) { should == false }
99
+ its(:oid?) { should == false }
100
+ end
101
+
102
+ context "a genus" do
103
+ subject { Pebbles::Uid::Query.new('beast:*$*') }
104
+ its(:genus?) { should == true }
105
+ its(:genus) { should eq('beast') }
106
+ its(:species?) { should == false }
107
+ end
108
+
109
+ context "a species" do
110
+ subject { Pebbles::Uid::Query.new('beast.mythical.hairy:*$*') }
111
+ its(:species?) { should == true }
112
+ its(:species) { should eq('mythical.hairy') }
113
+ end
114
+
115
+ context "a path" do
116
+ subject { Pebbles::Uid::Query.new('*:area51.*') }
117
+ its(:path?) { should == true }
118
+ end
119
+
120
+ context "one oid" do
121
+ subject { Pebbles::Uid::Query.new('*:*$yak') }
122
+ its(:oid?) { should == true }
123
+ its(:oid) { should == 'yak' }
124
+ end
125
+
126
+ end
127
+
128
+ end
data/spec/uid_spec.rb ADDED
@@ -0,0 +1,130 @@
1
+ require 'pebbles-uid'
2
+
3
+ describe Pebbles::Uid do
4
+
5
+ let(:uid) { 'post.card:tourism.norway.fjords$1234' }
6
+
7
+ describe "extracting single elements" do
8
+ specify { Pebbles::Uid.oid(uid).should eq('1234') }
9
+ specify { Pebbles::Uid.path(uid).should eq('tourism.norway.fjords') }
10
+ specify { Pebbles::Uid.genus(uid).should eq('post.card') }
11
+
12
+ describe "with missing oid" do
13
+ specify { Pebbles::Uid.oid('post:a.b.c').should eq(nil) }
14
+ end
15
+ end
16
+
17
+ describe "query" do
18
+ it "returns a query object" do
19
+ s = 'post:tourism.*$*'
20
+ Pebbles::Uid::Query.should_receive(:new).with s
21
+ query = Pebbles::Uid.query(s)
22
+ end
23
+ end
24
+
25
+ describe "requirements" do
26
+ it "must have a genus" do
27
+ ->{ Pebbles::Uid.new(':tourism.norway$1') }.should raise_error(ArgumentError)
28
+ end
29
+
30
+ it "must have a realm" do
31
+ ->{ Pebbles::Uid.new('post:$1') }.should raise_error(ArgumentError)
32
+ end
33
+
34
+ it "can skip the oid" do
35
+ ->{ Pebbles::Uid.new('post:tourism') }.should_not raise_error
36
+ end
37
+
38
+ it "doesn't accept wildcard genus" do
39
+ ->{ Pebbles::Uid.new('post.*:tourism$1') }.should raise_error(ArgumentError)
40
+ ->{ Pebbles::Uid.new('post|card:tourism$1') }.should raise_error(ArgumentError)
41
+ ->{ Pebbles::Uid.new('post.^b.c:tourism$1') }.should raise_error(ArgumentError)
42
+ end
43
+
44
+ it "doesn't accept wildcard paths" do
45
+ ->{ Pebbles::Uid.new('post:tourism.*$1') }.should raise_error(ArgumentError)
46
+ ->{ Pebbles::Uid.new('post:tourism|blogging$1') }.should raise_error(ArgumentError)
47
+ ->{ Pebbles::Uid.new('post:tourism.^b.c$1') }.should raise_error(ArgumentError)
48
+ end
49
+
50
+ it "doesn't accept wildcard oid" do
51
+ ->{ Pebbles::Uid.new('post:tourism$*') }.should raise_error(ArgumentError)
52
+ end
53
+ end
54
+
55
+ subject { Pebbles::Uid.new(uid) }
56
+
57
+ its(:to_s) { should eq(uid) }
58
+ its(:realm) { should eq('tourism') }
59
+ its(:genus) { should eq('post.card') }
60
+ its(:species) { should eq('card') }
61
+ its(:path) { should eq('tourism.norway.fjords') }
62
+ its(:oid) { should eq('1234') }
63
+ its(:oid?) { should == true }
64
+
65
+ its(:cache_key) { should eq('post.card:tourism.*$1234') }
66
+
67
+ context "when pending creation" do
68
+
69
+ let(:uid) { 'post.doc:universities.europe.norway' }
70
+ subject { Pebbles::Uid.new(uid) }
71
+
72
+ its(:to_s) { should eq(uid) }
73
+ its(:oid) { should be_nil }
74
+
75
+ end
76
+
77
+ context "paths" do
78
+ ["abc123", "abc.123", "abc.de-f.123"].each do |path|
79
+ specify "#{path} is a valid path" do
80
+ Pebbles::Uid.valid_path?(path).should == true
81
+ end
82
+ end
83
+
84
+ ["", ".", "..", "abc!"].each do |path|
85
+ specify "#{path} is not a valid path" do
86
+ Pebbles::Uid.valid_path?(path).should == false
87
+ end
88
+ end
89
+ end
90
+
91
+ context "genus" do
92
+ %w(- . _ 8).each do |char|
93
+ it "accepts #{char} in a genus" do
94
+ Pebbles::Uid.valid_genus?("uni#{char}corn").should == true
95
+ end
96
+ end
97
+
98
+ %w(! % { ? $).each do |char|
99
+ it "rejects #{char}" do
100
+ ->{ Pebbles::Uid.new("uni#{char}corn:mythical$1") }.should raise_error(ArgumentError)
101
+ end
102
+ end
103
+ end
104
+
105
+ context "oids" do
106
+ it "can be empty" do
107
+ Pebbles::Uid.new("beast:mythical").oid?.should == false
108
+ end
109
+
110
+ it "cannot contain pipes" do
111
+ Pebbles::Uid.valid_oid?("abc|xyz").should == false
112
+ end
113
+
114
+ it "cannot contain commas, either" do
115
+ Pebbles::Uid.valid_oid?("abc,xyz").should == false
116
+ end
117
+
118
+ it "can be empty" do
119
+ Pebbles::Uid.valid_oid?(nil).should == true
120
+ end
121
+
122
+ it "can be an empty string" do
123
+ Pebbles::Uid.valid_oid?("").should == true
124
+ end
125
+
126
+ it "is a black box" do
127
+ Pebbles::Uid.valid_oid?("holy+%^&*s!").should == true
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,53 @@
1
+ require 'pebbles-uid/wildcard'
2
+
3
+ describe Pebbles::Uid::Wildcard do
4
+
5
+ # This might not actually be necessary/relevant
6
+ specify "non-wildcard paths are valid" do
7
+ Pebbles::Uid::Wildcard.valid?('a.b.d').should == true
8
+ end
9
+
10
+ context "terminating asterisk representing entire label" do
11
+ [
12
+ '*', 'a.b.c.*', 'a.b|c.*', 'a.^b.c.*'
13
+ ].each do |path|
14
+ specify "#{path} is valid" do
15
+ Pebbles::Uid::Wildcard.valid?(path).should == true
16
+ end
17
+ end
18
+
19
+ ['*a', 'a*', '*.b', 'a.*.b'].each do |path|
20
+ specify "#{path} is invalid" do
21
+ Pebbles::Uid::Wildcard.valid?(path).should == false
22
+ end
23
+ end
24
+ end
25
+
26
+ context "pipes separate labels" do
27
+ ['a|b', 'a.b|c.d', 'a|b.c|d'].each do |path|
28
+ specify "#{path} is valid" do
29
+ Pebbles::Uid::Wildcard.valid?(path).should == true
30
+ end
31
+ end
32
+
33
+ ['|', 'a.|b', 'a.b|', 'a.|b.c', 'a.b|.c'].each do |path|
34
+ specify "#{path} is invalid" do
35
+ Pebbles::Uid::Wildcard.valid?(path).should == false
36
+ end
37
+ end
38
+ end
39
+
40
+ context "carets start an ancestry chain" do
41
+ ['^a', '^a.b', 'a.^b.c.*'].each do |path|
42
+ specify "#{path} is valid" do
43
+ Pebbles::Uid::Wildcard.valid?(path).should == true
44
+ end
45
+ end
46
+
47
+ ['^', '^.a', 'a^', 'a^b.c', '^a.^b'].each do |path|
48
+ specify "#{path} is invalid" do
49
+ Pebbles::Uid::Wildcard.valid?(path).should == false
50
+ end
51
+ end
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pebbles-uid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Katrina Owen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70278976164860 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70278976164860
25
+ description: Handle pebble UIDs conveniently.
26
+ email:
27
+ - katrina.owen@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/pebbles-uid.rb
38
+ - lib/pebbles-uid/conditions.rb
39
+ - lib/pebbles-uid/genus.rb
40
+ - lib/pebbles-uid/labels.rb
41
+ - lib/pebbles-uid/oid.rb
42
+ - lib/pebbles-uid/path.rb
43
+ - lib/pebbles-uid/query.rb
44
+ - lib/pebbles-uid/version.rb
45
+ - lib/pebbles-uid/wildcard.rb
46
+ - lib/pebbles/uid.rb
47
+ - pebbles-uid.gemspec
48
+ - spec/conditions_spec.rb
49
+ - spec/genus_spec.rb
50
+ - spec/labels_spec.rb
51
+ - spec/oid_spec.rb
52
+ - spec/path_spec.rb
53
+ - spec/query_spec.rb
54
+ - spec/uid_spec.rb
55
+ - spec/wildcard_spec.rb
56
+ homepage: ''
57
+ licenses: []
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 1.8.15
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Unique identifiers in the Pebblestack universe, in the format species[.genus]:path$oid,
80
+ where the path is a dot-delimited set of labels, the first of which (realm) is required.
81
+ test_files:
82
+ - spec/conditions_spec.rb
83
+ - spec/genus_spec.rb
84
+ - spec/labels_spec.rb
85
+ - spec/oid_spec.rb
86
+ - spec/path_spec.rb
87
+ - spec/query_spec.rb
88
+ - spec/uid_spec.rb
89
+ - spec/wildcard_spec.rb