structure 0.21.0 → 0.22.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/Gemfile CHANGED
@@ -1,6 +1,11 @@
1
1
  source :rubygems
2
2
  gemspec
3
+
3
4
  gem 'activesupport', '~> 3.0'
4
- gem 'json', :platform => [:mri_18, :jruby, :rbx]
5
+ if RUBY_VERSION.include? '1.8'
6
+ gem 'json'
7
+ gem 'minitest'
8
+ end
9
+
5
10
  gem 'pry' unless ENV['CI']
6
11
  gem 'rake'
data/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [![travis](https://secure.travis-ci.org/hakanensari/structure.png?branch=master)](http://travis-ci.org/hakanensari/structure)
4
4
 
5
- Structure is a typed, nestable key/value container.
5
+ Structure is a typed key/value container.
6
6
 
7
7
  class Person < Structure
8
- key :name
9
- many :friends
8
+ key :name
9
+ key :friends, Array, []
10
10
  end
11
11
 
12
12
  Please see the [wiki] [1] for more detail.
data/Rakefile CHANGED
@@ -4,6 +4,6 @@ require 'rake/testtask'
4
4
  task :default => :test
5
5
 
6
6
  Rake::TestTask.new do |test|
7
- test.libs << 'test'
7
+ test.libs += %w{lib test}
8
8
  test.test_files = FileList['test/**/*_test.rb']
9
9
  end
@@ -0,0 +1,58 @@
1
+ begin
2
+ JSON::JSON_LOADED
3
+ rescue NameError
4
+ require 'json'
5
+ end
6
+
7
+ class Structure
8
+ module JSON
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ # Converts structure to its JSON representation
14
+ #
15
+ # @param [Hash] args
16
+ # @return [JSON] a JSON representation of the structure
17
+ def to_json(*args)
18
+ { ::JSON.create_id => self.class.name }.
19
+ merge(@attributes).
20
+ to_json(*args)
21
+ end
22
+
23
+ if defined? ActiveSupport
24
+ # Converts structure to its JSON representation
25
+ #
26
+ # @param [Hash] options
27
+ # @return [JSON] a JSON representation of the structure
28
+ def as_json(options = nil)
29
+ subset = if options
30
+ if only = options[:only]
31
+ @attributes.slice(*Array.wrap(only))
32
+ elsif except = options[:except]
33
+ @attributes.except(*Array.wrap(except))
34
+ else
35
+ @attributes.dup
36
+ end
37
+ else
38
+ @attributes.dup
39
+ end
40
+
41
+ { ::JSON.create_id => self.class.name }.
42
+ merge(subset)
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ # Builds a structure out of its JSON representation
48
+ #
49
+ # @param [Hash] hsh a hashified JSON
50
+ # @return [Structure] a structure
51
+ def json_create(hsh)
52
+ hsh.delete('json_class')
53
+
54
+ new(hsh)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  class Structure
2
- VERSION = '0.21.0'
2
+ VERSION = '0.22.0'
3
3
  end
data/lib/structure.rb CHANGED
@@ -1,211 +1,190 @@
1
- begin
2
- JSON::JSON_LOADED
3
- rescue NameError
4
- require 'json'
5
- end
6
-
7
- # Structure is a nestable, typed key/value container.
1
+ # Structure is a typed key/value container
8
2
  #
3
+ # @example
9
4
  # class Person < Structure
10
- # key :name
11
- # many :friends
5
+ # key :name
6
+ # key :friends, Array, []
12
7
  # end
13
8
  #
14
9
  class Structure
15
- include Enumerable
16
-
17
- # A namespaced basic object.
18
- #
19
- # If running a legacy Ruby version, we either quote Builder's or
20
- # fabricate one ourselves.
21
- if defined?(BasicObject)
22
- BasicObject = ::BasicObject
23
- elsif defined?(BlankSlate)
24
- BasicObject = ::BlankSlate
25
- else
26
- class BasicObject
27
- instance_methods.each do |mth|
28
- undef_method(mth) unless mth =~ /\A(__|instance_eval)/
10
+ # Summon a Basic Object.
11
+ unless defined? BasicObject
12
+ if defined? BlankSlate
13
+ BasicObject = BlankSlate
14
+ else
15
+ class BasicObject
16
+ instance_methods.each do |mth|
17
+ undef_method(mth) unless mth =~ /__/
18
+ end
29
19
  end
30
20
  end
31
21
  end
32
22
 
33
- # A double that stands in for a yet-to-be-defined class. Otherwise
34
- # known as "lazy evaluation."
23
+ # A wrapper for lazy-evaluating undeclared classes
35
24
  #
36
- # Idea lifted from:
37
- # http://github.com/soveran/ohm/
38
- class Double < BasicObject
25
+ # @note Borrowed from the same-named class in Ohm
26
+ class Wrapper < BasicObject
27
+ # Wraps specified class in a wrapper if it is not already wrapped
28
+ #
29
+ # @param [Class] klass
30
+ # @return [Wrapper]
31
+ def self.wrap(klass)
32
+ klass.class == self ? klass : new(klass.to_s)
33
+ end
34
+
35
+ # Creates a new wrapper for specified class name
36
+ #
37
+ # @param [#to_s] name
39
38
  def initialize(name)
40
- @name = name
39
+ @name = name.to_s
40
+ end
41
+
42
+ # @return [Class] the class of the object
43
+ def class
44
+ Wrapper
41
45
  end
42
46
 
43
- def to_s
44
- @name.to_s
47
+ # Unwraps wrapped class
48
+ #
49
+ # @return [Class] the unwrapped class
50
+ def unwrap
51
+ @name.split('::').inject(::Kernel) do |parent, child|
52
+ parent.const_get(child)
53
+ end
45
54
  end
46
55
 
56
+ private
57
+
47
58
  def method_missing(mth, *args, &block)
48
59
  @unwrapped ? super : @unwrapped = true
49
60
  ::Kernel.const_get(@name).send(mth, *args, &block)
50
61
  ensure
51
62
  @unwrapped = false
52
- end; private :method_missing
63
+ end
53
64
  end
54
65
 
55
- class << self
56
- # Returns attribute keys and their default values.
57
- def defaults
58
- @defaults ||= {}
66
+ # A key definition
67
+ class Definition
68
+ # Creates a key definition
69
+ #
70
+ # @param [Class] type the key type
71
+ # @param [Object] default an optional default value
72
+ def initialize(type, default = nil)
73
+ @wrapper = Wrapper.wrap(type)
74
+ @default = default
75
+ end
76
+
77
+ # @return the default value for the key
78
+ attr :default
79
+
80
+ # @return [Class] the key type
81
+ def type
82
+ @type ||= @wrapper.unwrap
59
83
  end
60
84
 
61
- # Recreates a structure out of its JSON representation.
62
- def json_create(hsh)
63
- hsh.delete('json_class')
64
- new(hsh)
85
+ # Typecasts specified value
86
+ #
87
+ # @param [Object] val a value
88
+ # @raise [TypeError] value isn't a type
89
+ # @return [Object] a typecast value
90
+ def typecast(val)
91
+ if val.nil? || val.is_a?(type)
92
+ val.dup rescue val
93
+ elsif Kernel.respond_to?(type.to_s)
94
+ Kernel.send(type.to_s, val)
95
+ else
96
+ raise TypeError, "#{val} isn't a #{type}"
97
+ end
98
+ end
99
+ end
100
+
101
+ class << self
102
+ # @return [Hash] a collection of keys and their definitions
103
+ def blueprint
104
+ @blueprint ||= {}
65
105
  end
66
106
 
67
- # Defines an attribute.
107
+ # Defines a key
108
+ #
109
+ # @note Key type defaults to +String+ if not specified.
68
110
  #
69
- # Takes a name and, optionally, a type and default value.
70
- def key(name, type = nil, default = nil)
111
+ # @param [#to_sym] name the key name
112
+ # @param [Class] type an optional key type
113
+ # @param [Object] default an optional default value
114
+ # @raise [NameError] name is already taken
115
+ def key(name, type = String, default = nil)
71
116
  name = name.to_sym
72
117
 
73
118
  if method_defined?(name)
74
119
  raise NameError, "#{name} is taken"
75
120
  end
76
121
 
77
- if !default || !type || default.is_a?(type)
78
- defaults[name] = default
79
- else
122
+ if default && !default.is_a?(type)
80
123
  raise TypeError, "#{default} isn't a #{type}"
81
124
  end
82
125
 
83
- define_method(name) { attributes[name] }
126
+ # Add key to blueprint.
127
+ blueprint[name] = Definition.new(type, default)
84
128
 
85
- define_method("#{name}=") do |val|
86
- attributes[name] =
87
- if type.nil? || val.nil? || val.is_a?(type)
88
- val.dup rescue val
89
- elsif Kernel.respond_to?(type.to_s)
90
- Kernel.send(type.to_s, val)
91
- else
92
- raise TypeError, "#{val} isn't a #{type}"
93
- end
129
+ # Define getter.
130
+ define_method(name) do
131
+ @attributes[name]
94
132
  end
95
- end
96
-
97
- # Defines an attribute that is an array and defaults to an empty
98
- # one.
99
- def many(name)
100
- key name, Array, []
101
- end
102
133
 
103
- alias_method :new_original, :new
104
-
105
- def new(hsh)
106
- hsh = hsh.inject({}) do |a, (k, v)|
107
- a[Inflector.underscore(k)] =
108
- case v
109
- when Hash
110
- Structure.new(v)
111
- when Array
112
- v.map { |e| e.is_a?(Hash) ? Structure.new(e) : e }
113
- else
114
- v
115
- end
116
- a
117
- end
118
-
119
- klass = Class.new(Structure) do
120
- hsh.keys.each { |k| key k }
134
+ # Define setter.
135
+ define_method("#{name}=") do |val|
136
+ @attributes[name] = self.class.blueprint[name].typecast(val)
121
137
  end
122
-
123
- klass.new(hsh)
124
138
  end
125
139
 
126
- # Lazy-eval undefined constants, typically other structures,
127
- # assuming they will be defined later in the text.
128
- def const_missing(name)
129
- Double.new(name)
130
- end; private :const_missing
140
+ private
131
141
 
132
- def inherited(child)
133
- if self.eql? Structure
134
- class << child; alias_method :new, :new_original; end
135
- end
136
- end; private :inherited
142
+ def const_missing(name)
143
+ Wrapper.new(name)
144
+ end
137
145
  end
138
146
 
139
- # Creates a new structure.
147
+ # Builds a new structure
140
148
  #
141
- # A hash, if provided, seeds the attributes.
149
+ # @param [Hash] hsh a hash of key-value pairs
142
150
  def initialize(hsh = {})
143
- @attributes = defaults.inject({}) do |a, (k, v)|
144
- a[k] = v.dup rescue v
151
+ @attributes = blueprint.inject({}) do |a, (k, v)|
152
+ a[k] = v.default.dup rescue v.default
145
153
  a
146
154
  end
147
155
 
148
156
  hsh.each { |k, v| self.send("#{k}=", v) }
149
157
  end
150
158
 
151
- # The attributes that make up the structure.
152
- attr :attributes
153
-
154
- # Calls block once for each attribute in the structure, passing that
155
- # attribute as a parameter.
156
- def each(&block)
157
- attributes.each { |v| block.call(v) }
158
- end
159
-
160
- # Converts structure to a hash.
159
+ # @return [Hash] a hash representation of the structure
161
160
  def to_hash
162
- attributes.inject({}) do |a, (k, v)|
163
- a[k] =
164
- if v.respond_to?(:to_hash)
165
- v.to_hash
166
- elsif v.is_a?(Array)
167
- v.map { |e| e.respond_to?(:to_hash) ? e.to_hash : e }
168
- else
169
- v
170
- end
161
+ @attributes.inject({}) do |a, (k, v)|
162
+ a[k] = if v.respond_to?(:to_hash)
163
+ v.to_hash
164
+ elsif v.is_a?(Array)
165
+ v.map { |e| e.respond_to?(:to_hash) ? e.to_hash : e }
166
+ else
167
+ v
168
+ end
171
169
 
172
170
  a
173
171
  end
174
172
  end
175
173
 
176
- # Converts structure to its JSON representation.
177
- def to_json(*args)
178
- { JSON.create_id => self.class.name }.
179
- merge(attributes).
180
- to_json(*args)
181
- end
182
-
183
- # Compares this object with another object for equality. A Structure
184
- # is equal to the other object when both are of the same class and
185
- # the their attributes are the same.
174
+ # Compares this object with another object for equality
175
+ #
176
+ # A structure is equal to another object when both are of the same
177
+ # class and their attributes are the same.
178
+ #
179
+ # @param [Object] other another object
180
+ # @return [true, false]
186
181
  def ==(other)
187
182
  other.is_a?(self.class) && attributes == other.attributes
188
183
  end
189
184
 
190
- def defaults
191
- self.class.defaults
192
- end; private :defaults
185
+ private
193
186
 
194
- module Inflector
195
- # Makes an underscored, lowercase form from the expression in the
196
- # string.
197
- #
198
- # You know where this is lifted from.
199
- def self.underscore(camel_cased_word)
200
- word = camel_cased_word.to_s.dup
201
- word.gsub!(/::/, '/')
202
- word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
203
- word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
204
- word.tr!("-", "_")
205
- word.downcase!
206
- word
207
- end
187
+ def blueprint
188
+ self.class.blueprint
208
189
  end
209
190
  end
210
-
211
- require 'structure/ext/active_support' if defined?(Rails)
data/structure.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |s|
9
9
  s.authors = ['Hakan Ensari']
10
10
  s.email = ['code@papercavalier.com']
11
11
  s.homepage = 'http://github.com/hakanensari/structure'
12
- s.summary = 'A typed, nestable key/value container'
13
- s.description = 'Structure is a typed, nestable key/value container.'
12
+ s.summary = 'A typed key/value container'
13
+ s.description = 'Structure is a typed key/value container.'
14
14
 
15
15
  s.rubyforge_project = 'structure'
16
16
 
@@ -1,51 +1,33 @@
1
- require 'bundler/setup'
2
- require 'test/unit'
1
+ require 'minitest/autorun'
2
+
3
3
  begin
4
4
  require 'pry'
5
5
  rescue LoadError
6
6
  end
7
- require File.expand_path('../../lib/structure', __FILE__)
7
+
8
+ require 'structure'
9
+ require 'structure/json'
8
10
 
9
11
  class Person < Structure
10
- key :name
11
- key :location, Location
12
- many :friends
12
+ key :name
13
+ key :friends, Array, []
14
+ key :city, City
13
15
  end
14
16
 
15
- class Location < Structure
16
- key :lon, Float
17
- key :lat, Float
17
+ class City < Structure
18
+ key :name
18
19
  end
19
20
 
20
- class TestStructure < Test::Unit::TestCase
21
- def test_double
22
- double = Structure::Double.new(:Foo)
23
- assert_equal 'Foo', "#{double}"
24
- assert_raise(NameError) { double.class }
25
- ::Kernel.const_set(:Foo, 1)
26
- assert_kind_of Fixnum, double
27
- end
28
-
29
- def test_anonymous
30
- hsh = { 'FirstName' => 'John', 'LastName' => 'Doe' }
31
- str = Structure.new(hsh)
32
- assert_equal hsh['FirstName'], str.first_name
33
- assert_equal hsh['LastName'], str.last_name
34
- assert_raise(NoMethodError) { str.FirstName }
35
- end
21
+ class TestStructure < MiniTest::Unit::TestCase
22
+ def test_lazy_evaluation
23
+ wrapper = Structure::Wrapper.new(:Foo)
24
+ assert_raises(NameError) { wrapper.bar }
25
+ assert_raises(NameError) { wrapper.unwrap.bar }
36
26
 
37
- def test_anonymous_recursion
38
- hsh = { 'Name' => 'John',
39
- 'Address' => { 'City' => 'Oslo' },
40
- 'Friends' => [{ 'Name' => 'Jane' }]
41
- }
42
- str = Structure.new(hsh)
43
- assert_equal hsh['Address']['City'], str.address.city
44
- assert_equal hsh['Friends'].first['Name'], str.friends.first.name
45
- end
46
-
47
- def test_enumeration
48
- assert_respond_to Person.new, :map
27
+ klass = Class.new { def self.bar; end }
28
+ ::Kernel.const_set(:Foo, klass)
29
+ assert_respond_to wrapper, :bar
30
+ assert_equal Foo, wrapper.unwrap
49
31
  end
50
32
 
51
33
  def test_accessors
@@ -54,24 +36,24 @@ class TestStructure < Test::Unit::TestCase
54
36
  end
55
37
 
56
38
  def test_key_errors
57
- assert_raise(NameError) { Person.key :class }
58
- assert_raise(TypeError) { Person.key :foo, String, 1 }
39
+ assert_raises(NameError) { Person.key :class }
40
+ assert_raises(TypeError) { Person.key :foo, Hash, 1 }
59
41
  end
60
42
 
61
43
  def test_key_defaults
62
44
  assert_equal [], Person.new.friends
63
45
  end
64
46
 
65
- def test_typechecking
66
- loc = Location.new
67
- loc.lon = "1"
68
- assert_kind_of Float, loc.lon
47
+ def test_typecasting
48
+ person = Person.new
49
+ person.name = 123
50
+ assert_kind_of String, person.name
69
51
 
70
- loc.lon = nil
71
- assert_nil loc.lon
52
+ person.name = nil
53
+ assert_nil person.name
72
54
  end
73
55
 
74
- def test_array_type
56
+ def test_many_relationship
75
57
  person = Person.new
76
58
  assert_equal [], person.friends
77
59
 
@@ -80,26 +62,36 @@ class TestStructure < Test::Unit::TestCase
80
62
  assert_equal 0, person.friends.first.friends.size
81
63
  end
82
64
 
83
- # def test_to_hash
84
- # person = Person.new(:name => 'John')
85
- # person.friends << Person.new(:name => 'Jane')
86
- # assert_equal 'John', person.to_hash[:name]
87
- # assert_equal 'Jane', person.to_hash[:friends].first[:name]
88
- # end
65
+ def test_new
66
+ person = Person.new(:name => 'John')
67
+ assert_equal 'John', person.name
68
+
69
+ other = Person.new(:name => 'Jane', :friends => [person])
70
+ assert_equal 'John', other.friends.first.name
71
+ end
72
+
73
+ def test_to_hash
74
+ person = Person.new(:name => 'John')
75
+ person.friends << Person.new(:name => 'Jane')
76
+ hash = person.to_hash
77
+
78
+ assert_equal 'John', hash[:name]
79
+ assert_equal 'Jane', hash[:friends].first[:name]
80
+ end
89
81
 
90
82
  def test_json
91
- person = Person.new
92
- person.friends << Person.new
83
+ Person.send :include, Structure::JSON
84
+
85
+ person = Person.new(:name => 'John')
86
+ person.friends << Person.new(:name => 'Jane')
93
87
  json = person.to_json
94
88
  assert_kind_of Person, JSON.parse(json)
95
89
  assert_kind_of Person, JSON.parse(json).friends.first
96
-
97
90
  assert_equal false, person.respond_to?(:as_json)
98
91
 
99
92
  require 'active_support/ordered_hash'
100
93
  require 'active_support/json'
101
- require 'structure/ext/active_support'
102
-
94
+ load 'structure/json.rb'
103
95
  assert_equal true, person.as_json(:only => :name).has_key?(:name)
104
96
  assert_equal false, person.as_json(:except => :name).has_key?(:name)
105
97
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: structure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.22.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,9 +9,9 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-10 00:00:00.000000000Z
12
+ date: 2011-10-26 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: Structure is a typed, nestable key/value container.
14
+ description: Structure is a typed key/value container.
15
15
  email:
16
16
  - code@papercavalier.com
17
17
  executables: []
@@ -25,7 +25,7 @@ files:
25
25
  - README.md
26
26
  - Rakefile
27
27
  - lib/structure.rb
28
- - lib/structure/ext/active_support.rb
28
+ - lib/structure/json.rb
29
29
  - lib/structure/version.rb
30
30
  - structure.gemspec
31
31
  - test/structure_test.rb
@@ -43,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
43
43
  version: '0'
44
44
  segments:
45
45
  - 0
46
- hash: -3352714224337607479
46
+ hash: 2031698207987898845
47
47
  required_rubygems_version: !ruby/object:Gem::Requirement
48
48
  none: false
49
49
  requirements:
@@ -52,12 +52,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
52
  version: '0'
53
53
  segments:
54
54
  - 0
55
- hash: -3352714224337607479
55
+ hash: 2031698207987898845
56
56
  requirements: []
57
57
  rubyforge_project: structure
58
- rubygems_version: 1.8.6
58
+ rubygems_version: 1.8.10
59
59
  signing_key:
60
60
  specification_version: 3
61
- summary: A typed, nestable key/value container
61
+ summary: A typed key/value container
62
62
  test_files:
63
63
  - test/structure_test.rb
@@ -1,19 +0,0 @@
1
- class Structure
2
- # Converts structure to a JSON representation.
3
- def as_json(options = nil)
4
- subset = if options
5
- if only = options[:only]
6
- attributes.slice(*Array.wrap(only))
7
- elsif except = options[:except]
8
- attributes.except(*Array.wrap(except))
9
- else
10
- attributes.dup
11
- end
12
- else
13
- attributes.dup
14
- end
15
-
16
- { JSON.create_id => self.class.name }.
17
- merge(subset)
18
- end
19
- end