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 +34 -16
- data/Rakefile +0 -1
- data/lib/structure.rb +62 -27
- data/lib/structure/version.rb +1 -1
- data/spec/models/person.rb +1 -1
- data/spec/structure/json_spec.rb +3 -3
- data/spec/structure_spec.rb +99 -13
- data/structure.gemspec +5 -5
- metadata +8 -8
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
Structure
|
2
2
|
=========
|
3
3
|
|
4
|
-
Structure is a better
|
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
|
-
|
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
|
-
|
44
|
-
:age => 28)
|
45
|
+
Typecast values:
|
45
46
|
|
46
|
-
|
47
|
-
|
47
|
+
p1 = Person.new :name => 'John'
|
48
|
+
p1.age = '28'
|
49
|
+
p1.age
|
50
|
+
=> 28
|
48
51
|
|
49
|
-
|
52
|
+
Use ORM-esque association idioms:
|
50
53
|
|
51
|
-
|
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 =
|
56
|
-
=> {"json_class":"Person","name":"John","age":28,"friends":[{"json_class":"Person","name":
|
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.
|
60
|
-
=>
|
61
|
-
|
62
|
-
|
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
data/lib/structure.rb
CHANGED
@@ -1,61 +1,88 @@
|
|
1
|
-
|
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
|
-
|
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
|
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
|
-
|
28
|
-
|
29
|
-
|
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.
|
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,
|
51
|
-
# all values default to nil.
|
83
|
+
# Optionally, seeds the structure with a hash of attributes.
|
52
84
|
def initialize(seed = {})
|
53
|
-
|
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}"
|
127
|
+
raise TypeError, "can't modify frozen #{self.class}"
|
93
128
|
end
|
94
129
|
@attributes
|
95
130
|
end
|
data/lib/structure/version.rb
CHANGED
data/spec/models/person.rb
CHANGED
data/spec/structure/json_spec.rb
CHANGED
@@ -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":
|
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
|
25
|
+
it "loads them into their corresponding structures" do
|
26
26
|
json = person.to_json
|
27
|
-
JSON.parse(json).friends.first.
|
27
|
+
JSON.parse(json).friends.first.should be_a Person
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
data/spec/structure_spec.rb
CHANGED
@@ -4,11 +4,10 @@ describe Structure do
|
|
4
4
|
let(:person) { Person.new }
|
5
5
|
|
6
6
|
it "is enumerable" do
|
7
|
-
person.
|
8
|
-
person.map { |key, value| value }.should include "Joe"
|
7
|
+
person.should respond_to :map
|
9
8
|
end
|
10
9
|
|
11
|
-
context "when
|
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
|
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 :
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
45
|
-
it "does not
|
46
|
-
|
47
|
-
|
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 = "
|
12
|
+
s.summary = "A better Struct"
|
13
13
|
s.description = <<-END_OF_DESCRIPTION.strip
|
14
|
-
Structure is a better Struct
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
-
|
7
|
+
- 3
|
8
8
|
- 0
|
9
|
-
version: 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-
|
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
|
-
|
54
|
-
|
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:
|
107
|
+
summary: A better Struct
|
108
108
|
test_files:
|
109
109
|
- spec/models/person.rb
|
110
110
|
- spec/spec_helper.rb
|