projectile 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +16 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +14 -0
- data/README.md +95 -0
- data/Rakefile +12 -0
- data/app/app_delegate.rb +5 -0
- data/app/comment.rb +14 -0
- data/app/post.rb +28 -0
- data/lib/projectile.rb +9 -0
- data/lib/projectile/identity_map.rb +84 -0
- data/lib/projectile/model.rb +189 -0
- data/lib/projectile/value_transformer.rb +25 -0
- data/projectile.gemspec +13 -0
- data/spec/identity_map_spec.rb +55 -0
- data/spec/model_spec.rb +61 -0
- metadata +63 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
Projectile
|
2
|
+
==========
|
3
|
+
|
4
|
+
JSON model layer for RubyMotion applications. Inspired by [Mantle](https://github.com/github/Mantle).
|
5
|
+
|
6
|
+
# Example
|
7
|
+
|
8
|
+
Using the classic blog example, where we have posts and comments, here's how you could define your subclasses:
|
9
|
+
|
10
|
+
class Post < Model
|
11
|
+
set_attribute name: :id,
|
12
|
+
type: :integer,
|
13
|
+
key_path: "post.id"
|
14
|
+
|
15
|
+
set_attribute name: :title,
|
16
|
+
type: :string,
|
17
|
+
default: "",
|
18
|
+
key_path: "post.title"
|
19
|
+
|
20
|
+
set_attribute name: :body,
|
21
|
+
type: :string,
|
22
|
+
default: "",
|
23
|
+
key_path: "post.body"
|
24
|
+
|
25
|
+
set_attribute name: :created_at,
|
26
|
+
type: :date,
|
27
|
+
key_path: "post.created_at"
|
28
|
+
|
29
|
+
set_relationship name: :comments,
|
30
|
+
class_name: :Comment,
|
31
|
+
default: [],
|
32
|
+
key_path: "post.comments"
|
33
|
+
end
|
34
|
+
|
35
|
+
class Comment < Model
|
36
|
+
set_attribute name: :id,
|
37
|
+
type: :integer,
|
38
|
+
key_path: "comment.id"
|
39
|
+
|
40
|
+
set_attribute name: :text,
|
41
|
+
type: :string,
|
42
|
+
default: "",
|
43
|
+
key_path: "comment.text"
|
44
|
+
end
|
45
|
+
|
46
|
+
A `Post` has attributes `id`, `title`, `body`, `created_at`, and a to-many relationship to `Comment`.
|
47
|
+
A `Comment` has attributes `id` and `text`.
|
48
|
+
|
49
|
+
Default values are set during object initialization. i.e, `Model#new`.
|
50
|
+
|
51
|
+
Key paths define where the values are located in a JSON `Hash`. This is so we know where the value exists during `#new`, and also helps us recreate the `Hash` in `#to_hash`.
|
52
|
+
|
53
|
+
# Types
|
54
|
+
|
55
|
+
- `integer`
|
56
|
+
- `float`
|
57
|
+
- `boolean`
|
58
|
+
- `date`, only supports `yyyy-MM-dd'T'HH:mm:ssZZZZZ` right now.
|
59
|
+
- `url`, just turns the string into a `NSURL`
|
60
|
+
- `string`
|
61
|
+
|
62
|
+
# Identity Map
|
63
|
+
|
64
|
+
To use an identity map, put these lines at the top of a class definition:
|
65
|
+
|
66
|
+
include IdentityMap
|
67
|
+
establish_identity_on :id
|
68
|
+
|
69
|
+
What this means:
|
70
|
+
|
71
|
+
1. Defines equivalence based on the `id` attribute
|
72
|
+
2. Holds all instances of class, keyed by `id`, in a class instance variable `@@identity_map`.
|
73
|
+
|
74
|
+
Use `#merge_or_insert` instead of `#new` from now on:
|
75
|
+
|
76
|
+
# this will either create a brand new post object, or merge with an existing one.
|
77
|
+
json = {
|
78
|
+
"post" => {
|
79
|
+
"id" => 1,
|
80
|
+
"title" => "Hehehoho",
|
81
|
+
"body" => "La Li Lu Le Lo",
|
82
|
+
"created_at" => "2013-08-02T16:44:34-08:00"
|
83
|
+
}
|
84
|
+
}
|
85
|
+
@post = Post.merge_or_insert json
|
86
|
+
|
87
|
+
You can use `Post[1]` to fetch the above object from anywhere. Kind of useful in the REPL.
|
88
|
+
|
89
|
+
# Misc
|
90
|
+
|
91
|
+
I'm very open to ideas, please create issues, fork, whatever. Oh yeah, MIT.
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
$:.unshift("/Library/RubyMotion/lib")
|
3
|
+
require 'motion/project/template/ios'
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.require
|
6
|
+
|
7
|
+
|
8
|
+
Motion::Project::App.setup do |app|
|
9
|
+
# Use `rake config' to see complete project settings.
|
10
|
+
app.name = 'projectile'
|
11
|
+
app.deployment_target = '6.0'
|
12
|
+
end
|
data/app/app_delegate.rb
ADDED
data/app/comment.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class Comment < Model
|
2
|
+
include IdentityMap
|
3
|
+
establish_identity_on :id
|
4
|
+
|
5
|
+
set_attribute name: :id,
|
6
|
+
type: :integer,
|
7
|
+
default: -1,
|
8
|
+
key_path: "comment.id"
|
9
|
+
|
10
|
+
set_attribute name: :text,
|
11
|
+
type: :string,
|
12
|
+
default: "",
|
13
|
+
key_path: "comment.text"
|
14
|
+
end
|
data/app/post.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
class Post < Model
|
2
|
+
include IdentityMap
|
3
|
+
establish_identity_on :id
|
4
|
+
|
5
|
+
set_attribute name: :id,
|
6
|
+
type: :integer,
|
7
|
+
default: -1,
|
8
|
+
key_path: "post.id"
|
9
|
+
|
10
|
+
set_attribute name: :title,
|
11
|
+
type: :string,
|
12
|
+
default: "",
|
13
|
+
key_path: "post.title"
|
14
|
+
|
15
|
+
set_attribute name: :body,
|
16
|
+
type: :string,
|
17
|
+
default: "",
|
18
|
+
key_path: "post.body"
|
19
|
+
|
20
|
+
set_attribute name: :created_at,
|
21
|
+
type: :date,
|
22
|
+
key_path: "post.created_at"
|
23
|
+
|
24
|
+
set_relationship name: :comments,
|
25
|
+
class_name: :Comment,
|
26
|
+
default: [],
|
27
|
+
key_path: "post.comments"
|
28
|
+
end
|
data/lib/projectile.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
unless defined?(Motion::Project::Config)
|
2
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
3
|
+
end
|
4
|
+
|
5
|
+
Motion::Project::App.setup do |app|
|
6
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'projectile/*.rb')).each do |file|
|
7
|
+
app.files.unshift(file)
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module IdentityMap
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
def [](x)
|
10
|
+
self.identity_map[x]
|
11
|
+
end
|
12
|
+
|
13
|
+
def metaclass
|
14
|
+
class << self; self; end
|
15
|
+
end
|
16
|
+
|
17
|
+
def establish_identity_on(attr_name)
|
18
|
+
define_method("==") do |other|
|
19
|
+
return false unless self.is_a?(other.class)
|
20
|
+
self.send(attr_name) == other.send(attr_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
metaclass.instance_eval do
|
24
|
+
define_method("merge_or_insert") do |json|
|
25
|
+
return nil unless json
|
26
|
+
attribute = self.get_attributes.find {|a| a[:name] == attr_name}
|
27
|
+
key_path = attribute[:key_path]
|
28
|
+
identity_key = json.valueForKeyPath(key_path)
|
29
|
+
return nil unless identity_key
|
30
|
+
new_model = self.new json
|
31
|
+
old_model = self.identity_map[identity_key]
|
32
|
+
if old_model
|
33
|
+
old_model.merge_with_model(new_model)
|
34
|
+
else
|
35
|
+
self.identity_map[identity_key] = new_model
|
36
|
+
end
|
37
|
+
(old_model || new_model)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def identity_map
|
44
|
+
@identity_map ||= Hash.new
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def merge_with_model(model)
|
49
|
+
return unless self.is_a?(model.class)
|
50
|
+
|
51
|
+
self.class.get_relationships.each do |relationship|
|
52
|
+
name = relationship[:name]
|
53
|
+
model_value = model.send("#{name}")
|
54
|
+
self.send("#{name}=", model_value) unless model_value.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
self.class.get_attributes.each do |attribute|
|
58
|
+
name = attribute[:name]
|
59
|
+
model_value = model.send("#{name}")
|
60
|
+
default = attribute[:default]
|
61
|
+
self.send("#{name}=", model_value) unless model_value.nil? || model_value == default
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def merge_with_json(json)
|
66
|
+
self.class.get_relationships.each do |relationship|
|
67
|
+
name = relationship[:name]
|
68
|
+
default = relationship[:default]
|
69
|
+
key_path = relationship[:key_path]
|
70
|
+
json_value = json.valueForKeyPath(key_path)
|
71
|
+
self.send("#{name}=", json_value) unless json_value.nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
self.class.get_attributes.each do |attribute|
|
75
|
+
name = attribute[:name]
|
76
|
+
default = attribute[:default]
|
77
|
+
key_path = attribute[:key_path]
|
78
|
+
default = attribute[:default]
|
79
|
+
json_value = json.valueForKeyPath(key_path)
|
80
|
+
self.send("#{name}=", json_value) unless json_value.nil? || json_value == default
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
class Model
|
2
|
+
include ValueTransformer
|
3
|
+
|
4
|
+
# All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull.
|
5
|
+
register_value_transformer :type => :boolean,
|
6
|
+
:to => (lambda {|value| !!value}),
|
7
|
+
:from => (lambda {|value| value})
|
8
|
+
|
9
|
+
register_value_transformer :type => :integer,
|
10
|
+
:to => (lambda {|value| value.to_i}),
|
11
|
+
:from => (lambda {|value| value})
|
12
|
+
|
13
|
+
register_value_transformer :type => :float,
|
14
|
+
:to => (lambda {|value| value.to_f}),
|
15
|
+
:from => (lambda {|value| value})
|
16
|
+
|
17
|
+
register_value_transformer :type => :date,
|
18
|
+
:to => (lambda {|value| value.is_a?(Time) ? value : date_formatter.dateFromString(value.to_s)}),
|
19
|
+
:from => (lambda {|value| date_formatter.stringFromDate(value)})
|
20
|
+
|
21
|
+
register_value_transformer :type => :url,
|
22
|
+
:to => (lambda {|value| value.is_a?(NSURL) ? value : NSURL.URLWithString(value.to_s)}),
|
23
|
+
:from => (lambda {|value| value.to_s})
|
24
|
+
|
25
|
+
register_value_transformer :type => :string,
|
26
|
+
:to => (lambda {|value| value.to_s}),
|
27
|
+
:from => (lambda {|value| value})
|
28
|
+
|
29
|
+
class << self
|
30
|
+
|
31
|
+
# these methods are needed because subclasses do not inherit class instance variables
|
32
|
+
def get_attributes
|
33
|
+
attributes = []
|
34
|
+
klass = self
|
35
|
+
while klass.respond_to? :attributes
|
36
|
+
attributes += klass.attributes
|
37
|
+
klass = klass.superclass
|
38
|
+
end
|
39
|
+
attributes
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_relationships
|
43
|
+
relationships = []
|
44
|
+
klass = self
|
45
|
+
while klass.respond_to? :relationships
|
46
|
+
relationships += klass.relationships
|
47
|
+
klass = klass.superclass
|
48
|
+
end
|
49
|
+
relationships
|
50
|
+
end
|
51
|
+
|
52
|
+
def attributes
|
53
|
+
@attributes ||= []
|
54
|
+
end
|
55
|
+
|
56
|
+
def set_attribute(attribute)
|
57
|
+
name = attribute[:name]
|
58
|
+
type = attribute[:type]
|
59
|
+
default = attribute[:default]
|
60
|
+
attribute[:key_path] ||= name
|
61
|
+
|
62
|
+
attr_accessor name
|
63
|
+
|
64
|
+
# setter
|
65
|
+
define_method("#{name}=") do |value|
|
66
|
+
transformer = self.class.value_transformers[type][:to]
|
67
|
+
new_value = transformer.call(value) if transformer
|
68
|
+
self.willChangeValueForKey name
|
69
|
+
self.instance_variable_set("@#{name}", new_value)
|
70
|
+
self.didChangeValueForKey name
|
71
|
+
end
|
72
|
+
|
73
|
+
self.attributes << attribute
|
74
|
+
end
|
75
|
+
|
76
|
+
def relationships
|
77
|
+
@relationships ||= []
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_relationship(relationship)
|
81
|
+
name = relationship[:name]
|
82
|
+
class_name = relationship[:class_name]
|
83
|
+
default = relationship[:default]
|
84
|
+
relationship[:key_path] ||= name
|
85
|
+
|
86
|
+
attr_accessor name
|
87
|
+
|
88
|
+
# setter
|
89
|
+
define_method("#{name}=") do |value|
|
90
|
+
cls = Kernel.const_get(class_name.capitalize)
|
91
|
+
new_value = case value
|
92
|
+
when Hash
|
93
|
+
cls.include?(IdentityMap) ? cls.merge_or_insert(value) : cls.new(value)
|
94
|
+
when Array
|
95
|
+
value.map {|e| (e.is_a?(cls) ? e : (cls.include?(IdentityMap) ? cls.merge_or_insert(e) : cls.new(e)))}
|
96
|
+
when cls
|
97
|
+
value
|
98
|
+
else
|
99
|
+
nil
|
100
|
+
end if value
|
101
|
+
|
102
|
+
self.willChangeValueForKey name
|
103
|
+
self.instance_variable_set("@#{name}", new_value)
|
104
|
+
self.didChangeValueForKey name
|
105
|
+
end
|
106
|
+
|
107
|
+
self.relationships << relationship
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_hash(serialized_hash_by_model={})
|
112
|
+
hash = {}
|
113
|
+
serialized_hash_by_model[self] = hash
|
114
|
+
|
115
|
+
self.class.get_attributes.each do |attribute|
|
116
|
+
name = attribute[:name]
|
117
|
+
key_path = attribute[:key_path]
|
118
|
+
type = attribute[:type]
|
119
|
+
default = attribute[:default]
|
120
|
+
|
121
|
+
# grab value
|
122
|
+
model_value = self.send("#{name}")
|
123
|
+
next unless model_value
|
124
|
+
|
125
|
+
# create intermediate hashes
|
126
|
+
components = key_path.split(".")
|
127
|
+
inner_hash = hash
|
128
|
+
components.each_with_index do |component, index|
|
129
|
+
if index == components.count-1
|
130
|
+
transformer = self.class.value_transformers[type][:from]
|
131
|
+
inner_hash[component] = transformer.call(model_value)
|
132
|
+
else
|
133
|
+
hash[component] ||= Hash.new
|
134
|
+
inner_hash = hash[component]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
self.class.get_relationships.each do |relationship|
|
140
|
+
name = relationship[:name]
|
141
|
+
key_path = relationship[:key_path]
|
142
|
+
class_name = relationship[:class_name]
|
143
|
+
|
144
|
+
model_value = self.send("#{name}")
|
145
|
+
next unless model_value
|
146
|
+
|
147
|
+
components = key_path.split(".")
|
148
|
+
inner_hash = hash
|
149
|
+
components.each_with_index do |component, index|
|
150
|
+
if index == components.count-1
|
151
|
+
inner_hash[component] = case model_value
|
152
|
+
when Array
|
153
|
+
model_value.compact.map do |e|
|
154
|
+
serialized_hash_by_model[e] ? serialized_hash_by_model[e].clone : e.to_hash(serialized_hash_by_model)
|
155
|
+
end
|
156
|
+
when Model
|
157
|
+
serialized_hash_by_model[model_value] ? serialized_hash_by_model[model_value].clone : model_value.to_hash(serialized_hash_by_model)
|
158
|
+
else
|
159
|
+
end
|
160
|
+
else
|
161
|
+
hash[component] ||= Hash.new
|
162
|
+
inner_hash = hash[component]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
hash
|
168
|
+
end
|
169
|
+
|
170
|
+
def initialize(json={})
|
171
|
+
self.class.get_relationships.each do |relationship|
|
172
|
+
name = relationship[:name]
|
173
|
+
default = relationship[:default]
|
174
|
+
key_path = relationship[:key_path]
|
175
|
+
json_value = json.valueForKeyPath(key_path)
|
176
|
+
value_to_send = json_value.nil? ? default : json_value
|
177
|
+
self.send("#{name}=", value_to_send) unless value_to_send.nil?
|
178
|
+
end
|
179
|
+
|
180
|
+
self.class.get_attributes.each do |attribute|
|
181
|
+
name = attribute[:name]
|
182
|
+
default = attribute[:default]
|
183
|
+
key_path = attribute[:key_path]
|
184
|
+
json_value = json.valueForKeyPath(key_path)
|
185
|
+
value_to_send = json_value.nil? ? default : json_value
|
186
|
+
self.send("#{name}=", value_to_send) unless value_to_send.nil?
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ValueTransformer
|
2
|
+
module ClassMethods
|
3
|
+
def date_formatter
|
4
|
+
@@date_formatter ||= NSDateFormatter.alloc.init.tap do |date_formatter|
|
5
|
+
date_formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def value_transformers
|
10
|
+
@@value_transformers ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def register_value_transformer(options)
|
14
|
+
type = options[:type]
|
15
|
+
to = options[:to]
|
16
|
+
from = options[:from]
|
17
|
+
return if self.value_transformers.has_key? type
|
18
|
+
self.value_transformers[type] = {to: to, from: from}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.extend(ClassMethods)
|
24
|
+
end
|
25
|
+
end
|
data/projectile.gemspec
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.authors = ["John Z Wu"]
|
3
|
+
gem.email = ["bogardon@gmail.com"]
|
4
|
+
gem.description = "Simple RubyMotion JSON model layer"
|
5
|
+
gem.summary = "Simple RubyMotion JSON model layer"
|
6
|
+
gem.homepage = "https://github.com/bogardon/projectile"
|
7
|
+
gem.files = `git ls-files`.split($\)
|
8
|
+
gem.test_files = gem.files.grep(%r{^spec/})
|
9
|
+
gem.name = "projectile"
|
10
|
+
gem.require_paths = ["lib"]
|
11
|
+
gem.version = "0.0.1"
|
12
|
+
gem.license = 'MIT'
|
13
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
describe "Identity Map" do
|
2
|
+
before do
|
3
|
+
@json1 = {
|
4
|
+
"post" => {
|
5
|
+
"id" => 1,
|
6
|
+
"title" => "Old Title",
|
7
|
+
"body" => "Old Body"
|
8
|
+
}
|
9
|
+
}
|
10
|
+
@post1 = Post.merge_or_insert @json1
|
11
|
+
|
12
|
+
@json2 = {
|
13
|
+
"post" => {
|
14
|
+
"id" => 1,
|
15
|
+
"title" => "New Title",
|
16
|
+
"comments" => [
|
17
|
+
{
|
18
|
+
"comment" => {
|
19
|
+
"id" => 1,
|
20
|
+
"text" => "Cool story bro."
|
21
|
+
}
|
22
|
+
},
|
23
|
+
{
|
24
|
+
"comment" => {
|
25
|
+
"id" => 2,
|
26
|
+
"text" => "La Li Lu Le Lo"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
]
|
30
|
+
}
|
31
|
+
}
|
32
|
+
@post2 = Post.merge_or_insert @json2
|
33
|
+
@comments2 = @post2.comments
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "merging" do
|
37
|
+
it "should have correct values in identity map" do
|
38
|
+
Post.identity_map.count.should == 1
|
39
|
+
Comment.identity_map.count.should == 2
|
40
|
+
@post1.should == @post2
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should merge in new value" do
|
44
|
+
@post1.title.should == @json2['post']['title']
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should not merge in nil value" do
|
48
|
+
@post1.body.should == @json1['post']['body']
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should replace relationships" do
|
52
|
+
@post1.comments.should == @comments2
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
describe "Model" do
|
2
|
+
before do
|
3
|
+
@json = {
|
4
|
+
"post" => {
|
5
|
+
"id" => 1,
|
6
|
+
"title" => "Projectile",
|
7
|
+
"body" => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
8
|
+
"created_at" => "2013-08-10T07:00:00-07:00",
|
9
|
+
"comments" => [
|
10
|
+
{
|
11
|
+
"comment" => {
|
12
|
+
"id" => 1,
|
13
|
+
"text" => "Cool story bro."
|
14
|
+
}
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"comment" => {
|
18
|
+
"id" => 2,
|
19
|
+
"text" => "La Li Lu Le Lo"
|
20
|
+
}
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
24
|
+
}
|
25
|
+
@post = Post.new @json
|
26
|
+
@comments = @post.comments
|
27
|
+
@empty_post = Post.new
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "populating attributes" do
|
31
|
+
it "has correct attributes" do
|
32
|
+
@post.id.should == 1
|
33
|
+
@post.title.should == @json['post']['title']
|
34
|
+
@post.body.should == @json['post']['body']
|
35
|
+
end
|
36
|
+
|
37
|
+
it "has correct defaults" do
|
38
|
+
@empty_post.id.should == -1
|
39
|
+
@empty_post.title.should == ""
|
40
|
+
@empty_post.body.should == ""
|
41
|
+
@empty_post.comments.should == []
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "populating relationships" do
|
46
|
+
it "has correct relationships" do
|
47
|
+
@comments.count.should == 2
|
48
|
+
@comments.first.id.should == 1
|
49
|
+
@comments.last.id.should == 2
|
50
|
+
@comments.first.text.should == "Cool story bro."
|
51
|
+
@comments.last.text.should == "La Li Lu Le Lo"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "generating hash" do
|
56
|
+
it "should be the same as json" do
|
57
|
+
@post.to_hash.should == @json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: projectile
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Z Wu
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-05 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Simple RubyMotion JSON model layer
|
15
|
+
email:
|
16
|
+
- bogardon@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- Gemfile.lock
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- app/app_delegate.rb
|
27
|
+
- app/comment.rb
|
28
|
+
- app/post.rb
|
29
|
+
- lib/projectile.rb
|
30
|
+
- lib/projectile/identity_map.rb
|
31
|
+
- lib/projectile/model.rb
|
32
|
+
- lib/projectile/value_transformer.rb
|
33
|
+
- projectile.gemspec
|
34
|
+
- spec/identity_map_spec.rb
|
35
|
+
- spec/model_spec.rb
|
36
|
+
homepage: https://github.com/bogardon/projectile
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.8.25
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Simple RubyMotion JSON model layer
|
61
|
+
test_files:
|
62
|
+
- spec/identity_map_spec.rb
|
63
|
+
- spec/model_spec.rb
|