projectile 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ .repl_history
2
+ build
3
+ tags
4
+ app/pixate_code.rb
5
+ resources/*.nib
6
+ resources/*.momd
7
+ resources/*.storyboardc
8
+ .DS_Store
9
+ nbproject
10
+ .redcar
11
+ #*#
12
+ *~
13
+ *.sw[po]
14
+ .eprj
15
+ .sass-cache
16
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,14 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ projectile (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+
10
+ PLATFORMS
11
+ ruby
12
+
13
+ DEPENDENCIES
14
+ projectile!
@@ -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
+
@@ -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
@@ -0,0 +1,5 @@
1
+ class AppDelegate
2
+ def application(application, didFinishLaunchingWithOptions:launchOptions)
3
+ true
4
+ end
5
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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