structure 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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