structure 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  Structure
2
2
  =========
3
3
 
4
- Structure is a better struct.
5
-
4
+ Structure is a better Struct and does wonders when modeling ephemeral
5
+ data fed in from an API.
6
6
 
7
7
  #_ d
8
8
  ##_ d#
@@ -30,33 +30,51 @@ Structure is a better struct.
30
30
  Usage
31
31
  -----
32
32
 
33
- Structure, like Struct, is great for defining ephemeral models.
33
+ Require:
34
34
 
35
35
  require 'structure'
36
36
 
37
+ Define a model:
38
+
37
39
  class Person < Structure
38
40
  key :name
39
41
  key :age, :type => Integer
40
- key :friends, :type => Array
42
+ key :friends, :type => Array, :value => []
41
43
  end
42
44
 
43
- john = Person.new(:name => "John",
44
- :age => 28)
45
+ Typecast values:
45
46
 
46
- jane = Person.new(:name => "Jane",
47
- :age => 24)
47
+ p1 = Person.new :name => 'John'
48
+ p1.age = '28'
49
+ p1.age
50
+ => 28
48
51
 
49
- john.friends = [jane]
52
+ Use ORM-esque association idioms:
50
53
 
51
- When it comes to dumping JSON, Structure is more aesthetically-minded.
54
+ p2 = Person.new :name => 'Jane'
55
+ p1.friends << p2
56
+
57
+ Dump good-looking JSON:
52
58
 
53
59
  require 'structure/json'
54
60
 
55
- json = john.to_json
56
- => {"json_class":"Person","name":"John","age":28,"friends":[{"json_class":"Person","name":"Jane","age":24,"friends":null}]}
61
+ json = p1.to_json
62
+ => {"json_class":"Person","name":"John","age":28,"friends":[{"json_class":"Person","name": null,"age":null,"friends":[]}]}
63
+
64
+ Load the JSON in a different app back into Ruby seamlessly, provided you
65
+ have the same models defined there:
57
66
 
58
67
  person = JSON.parse(json)
59
- person.friends.first.name
60
- => "Jane"
61
- person.friends.first.age
62
- => 24
68
+ person.friends.first.class
69
+ => Person
70
+
71
+ Types
72
+ -----
73
+
74
+ Structure supports the following types:
75
+ * Array
76
+ * Boolean
77
+ * Float
78
+ * Hash
79
+ * Integer
80
+ * String
data/Rakefile CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'bundler'
2
- require 'cucumber/rake/task'
3
2
  require 'rspec/core/rake_task'
4
3
 
5
4
  Bundler::GemHelper.install_tasks
data/lib/structure.rb CHANGED
@@ -1,61 +1,88 @@
1
- require 'json'
2
-
3
- # A better struct.
1
+ # A better Ruby Struct.
4
2
  class Structure
5
-
6
- # Mix in the Enumerable module.
7
3
  include Enumerable
8
4
 
9
- @@keys = []
5
+ # Ruby doesn't have a Boolean class, so let's feign one.
6
+ unless Object.const_defined?(:Boolean)
7
+ module ::Boolean; end
8
+ class ::TrueClass; include Boolean; end
9
+ class ::FalseClass; include Boolean; end
10
+ end
11
+
12
+ TYPES = [Array, Boolean, Float, Hash, Integer, String]
13
+
14
+ @@default_attributes = {}
10
15
 
11
16
  # Defines an attribute key.
12
17
  #
13
18
  # Takes a name and an optional hash of options. Available options are:
14
19
  #
15
- # * :type, which can be Integer, Float, String, and Array.
20
+ # * :type, which can be Array, Boolean, Float, Integer, JSON, Pathname,
21
+ # String, or URI. If not specified, type defaults to String.
22
+ # * :default, which sets the default value for the attribute.
16
23
  #
17
24
  # class Book
18
25
  # key :title, :type => String
19
- # key :authors, :type => Array
26
+ # key :authors, :type => Array, :default => []
20
27
  # end
21
28
  #
22
29
  def self.key(name, options={})
30
+ name = name.to_sym
23
31
  if method_defined?(name)
24
32
  raise NameError, "#{name} is already defined"
25
33
  end
26
34
 
27
- name = name.to_sym
28
- type = options[:type]
29
- @@keys << name
35
+ type = options[:type] || String
36
+ unless TYPES.include? type
37
+ raise TypeError, "#{type} is not a valid type"
38
+ end
39
+
40
+ default = options[:default]
41
+ unless default.nil? || default.is_a?(type)
42
+ raise TypeError, "#{default} is not #{%w{AEIOU}.include?(type.to_s[0]) ? 'an' : 'a'} #{type}"
43
+ end
44
+
45
+ @@default_attributes[name] = default
30
46
 
31
47
  module_eval do
32
48
 
49
+ # Define a proc to typecast value.
50
+ typecast =
51
+ if type == Boolean
52
+ lambda do |value|
53
+ case value
54
+ when String
55
+ !(value =~ /false/i)
56
+ else
57
+ !!value
58
+ end
59
+ end
60
+ elsif type == Hash
61
+ lambda do |value|
62
+ unless value.is_a? Hash
63
+ raise TypeError, "#{value} is not a Hash"
64
+ end
65
+ value
66
+ end
67
+ else
68
+ lambda { |value| Kernel.send(type.to_s, value) }
69
+ end
70
+
33
71
  # Define a getter.
34
72
  define_method(name) { @attributes[name] }
35
73
 
36
- # Define a setter. The setter will optionally typecast.
74
+ # Define a setter.
37
75
  define_method("#{name}=") do |value|
38
- modifiable[name] =
39
- if type && value
40
- Kernel.send(type.to_s, value)
41
- else
42
- value
43
- end
76
+ modifiable[name] = value.nil? ? nil : typecast.call(value)
44
77
  end
45
78
  end
46
79
  end
47
80
 
48
81
  # Creates a new structure.
49
82
  #
50
- # Optionally, populates the structure with a hash of attributes. Otherwise,
51
- # all values default to nil.
83
+ # Optionally, seeds the structure with a hash of attributes.
52
84
  def initialize(seed = {})
53
- @attributes =
54
- @@keys.inject({}) do |attributes, name|
55
- attributes[name] = nil
56
- attributes
57
- end
58
-
85
+ initialize_attributes
59
86
  seed.each { |key, value| self.send("#{key}=", value) }
60
87
  end
61
88
 
@@ -85,11 +112,19 @@ class Structure
85
112
 
86
113
  private
87
114
 
115
+ def initialize_attributes
116
+ @attributes =
117
+ @@default_attributes.inject({}) do |attributes, (key, value)|
118
+ attributes[key] = value
119
+ attributes
120
+ end
121
+ end
122
+
88
123
  def modifiable
89
124
  begin
90
125
  @modifiable = true
91
126
  rescue
92
- raise TypeError, "can't modify frozen #{self.class}", caller(3)
127
+ raise TypeError, "can't modify frozen #{self.class}"
93
128
  end
94
129
  @attributes
95
130
  end
@@ -1,3 +1,3 @@
1
1
  class Sucker
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -1,5 +1,5 @@
1
1
  class Person < Structure
2
2
  key :name
3
3
  key :age, :type => Integer
4
- key :friends
4
+ key :friends, :type => Array, :default => []
5
5
  end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe Structure do
4
4
  context "when `structure/json' is required" do
5
5
  let(:person) { Person.new(:name => 'Joe', :age => 28) }
6
- let(:json) { '{"json_class":"Person","name":"Joe","age":28,"friends":null}' }
6
+ let(:json) { '{"json_class":"Person","name":"Joe","age":28,"friends":[]}' }
7
7
 
8
8
  before do
9
9
  require 'structure/json'
@@ -22,9 +22,9 @@ describe Structure do
22
22
  person.friends = [Person.new(:name => 'Jane')]
23
23
  end
24
24
 
25
- it "loads nested structures from JSON" do
25
+ it "loads them into their corresponding structures" do
26
26
  json = person.to_json
27
- JSON.parse(json).friends.first.name.should eql 'Jane'
27
+ JSON.parse(json).friends.first.should be_a Person
28
28
  end
29
29
  end
30
30
  end
@@ -4,11 +4,10 @@ describe Structure do
4
4
  let(:person) { Person.new }
5
5
 
6
6
  it "is enumerable" do
7
- person.name ="Joe"
8
- person.map { |key, value| value }.should include "Joe"
7
+ person.should respond_to :map
9
8
  end
10
9
 
11
- context "when object is frozen" do
10
+ context "when frozen" do
12
11
  before do
13
12
  person.freeze
14
13
  end
@@ -25,29 +24,116 @@ describe Structure do
25
24
  %w{name name=}.each { |method| person.should respond_to method }
26
25
  end
27
26
 
28
- context "when name clashes with an existing method" do
27
+ context "when a key name clashes with a method name" do
29
28
  it "raises an error" do
30
29
  expect do
31
- Person.key :name
30
+ Person.key :class
32
31
  end.to raise_error NameError
33
32
  end
34
33
  end
35
34
 
35
+ context "when an invalid type is specified" do
36
+ it "raises an error" do
37
+ expect do
38
+ Person.key :location, :type => Object
39
+ end.to raise_error TypeError
40
+ end
41
+ end
42
+
43
+ context "when default value is not of the specified type" do
44
+ it "raises an error" do
45
+ expect do
46
+ Person.key :location, :type => String, :default => 0
47
+ end.to raise_error TypeError
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "attribute getter" do
53
+ it "returns the value of the attribute" do
54
+ person.instance_variable_get(:@attributes)[:name] = 'Joe'
55
+ person.name.should eql 'Joe'
56
+ end
57
+
58
+ context "when type is Array and default value is []" do
59
+ it "supports the `<<' idiom" do
60
+ person.friends << Person.new
61
+ person.friends.count.should eql 1
62
+ end
63
+ end
64
+ end
65
+
66
+ describe "attribute setter" do
67
+ it "sets the value of the attribute" do
68
+ person.name = "Joe"
69
+ person.instance_variable_get(:@attributes)[:name].should eql 'Joe'
70
+ end
71
+
36
72
  context "when a type is specified" do
37
- context "when setting the attribute to a non-nil value" do
38
- it "casts the value" do
39
- person.age = "28"
40
- person.age.should eql 28
73
+ it "casts the value" do
74
+ person.age = "28"
75
+ person.age.should be_an Integer
76
+ end
77
+ end
78
+
79
+ context "when a type is not specified" do
80
+ it "casts to String" do
81
+ person.name = 123
82
+ person.name.should be_a String
83
+ end
84
+ end
85
+
86
+ context "when type is Boolean" do
87
+ context "when default value is true" do
88
+ it "does not raise an invalid type error" do
89
+ expect do
90
+ Person.key :single, :type => Boolean, :default => true
91
+ end.not_to raise_error
41
92
  end
42
93
  end
43
94
 
44
- context "when setting the attribute to nil" do
45
- it "does not set the value" do
46
- person.age = nil
47
- person.age.should be_nil
95
+ context "when default value is false" do
96
+ it "does not raise an invalid type error" do
97
+ expect do
98
+ Person.key :married, :type => Boolean, :default => false
99
+ end.not_to raise_error
100
+ end
101
+ end
102
+ end
103
+
104
+ context "when type is Hash" do
105
+ before(:all) do
106
+ Person.key :education, :type => Hash
107
+ end
108
+
109
+ context "when setting to a value is not a Hash" do
110
+ it "raises an error" do
111
+ expect do
112
+ person.education = 'foo'
113
+ end.to raise_error TypeError
48
114
  end
49
115
  end
50
116
  end
117
+
118
+ context "when a default is specified" do
119
+ it "defaults to that value" do
120
+ Person.key :location, :default => 'New York'
121
+ person.location.should eql 'New York'
122
+ end
123
+ end
124
+
125
+ context "when a default is not specified" do
126
+ it "defaults to nil" do
127
+ person.age.should be_nil
128
+ end
129
+ end
130
+
131
+ context "when setting the value of an attribute to nil" do
132
+ it "does not typecast the value" do
133
+ person.age = nil
134
+ person.age.should be_a NilClass
135
+ end
136
+ end
51
137
  end
52
138
 
53
139
  describe ".new" do
data/structure.gemspec CHANGED
@@ -9,12 +9,12 @@ Gem::Specification.new do |s|
9
9
  s.authors = ["Paper Cavalier"]
10
10
  s.email = ["code@papercavalier.com"]
11
11
  s.homepage = "http://rubygems.com/gems/structure"
12
- s.summary = "Structure is a better Struct."
12
+ s.summary = "A better Struct"
13
13
  s.description = <<-END_OF_DESCRIPTION.strip
14
- Structure is a better Struct.
15
-
16
- Like Struct, it is great for setting up ephemeral models. It also handles
17
- typecasting and, unlike Struct, dumps nicely-formatted JSON.
14
+ Structure is a better Struct and does wonders when modeling ephemeral data
15
+ fed in from an API. It typecasts values, works with ORM-esque association
16
+ idioms, dumps good-looking JSON, and loads the same JSON seamlessly back
17
+ into Ruby.
18
18
  END_OF_DESCRIPTION
19
19
 
20
20
  s.rubyforge_project = "structure"
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
7
+ - 3
8
8
  - 0
9
- version: 0.2.0
9
+ version: 0.3.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Paper Cavalier
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-05-26 00:00:00 +01:00
17
+ date: 2011-05-27 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -48,10 +48,10 @@ dependencies:
48
48
  type: :development
49
49
  version_requirements: *id002
50
50
  description: |-
51
- Structure is a better Struct.
52
-
53
- Like Struct, it is great for setting up ephemeral models. It also handles
54
- typecasting and, unlike Struct, dumps nicely-formatted JSON.
51
+ Structure is a better Struct and does wonders when modeling ephemeral data
52
+ fed in from an API. It typecasts values, works with ORM-esque association
53
+ idioms, dumps good-looking JSON, and loads the same JSON seamlessly back
54
+ into Ruby.
55
55
  email:
56
56
  - code@papercavalier.com
57
57
  executables: []
@@ -104,7 +104,7 @@ rubyforge_project: structure
104
104
  rubygems_version: 1.3.7
105
105
  signing_key:
106
106
  specification_version: 3
107
- summary: Structure is a better Struct.
107
+ summary: A better Struct
108
108
  test_files:
109
109
  - spec/models/person.rb
110
110
  - spec/spec_helper.rb