motion-resource 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/Gemfile +8 -0
- data/README.md +53 -0
- data/Rakefile +14 -0
- data/app/app_delegate.rb +7 -0
- data/examples/Colr/.gitignore +13 -0
- data/examples/Colr/Rakefile +10 -0
- data/examples/Colr/app/app_delegate.rb +10 -0
- data/examples/Colr/app/color.rb +15 -0
- data/examples/Colr/app/color_view_controller.rb +46 -0
- data/examples/Colr/app/initializer.rb +2 -0
- data/examples/Colr/app/main_navigation_controller.rb +6 -0
- data/examples/Colr/app/tag.rb +5 -0
- data/examples/Colr/app/tags_view_controller.rb +23 -0
- data/lib/motion-resource.rb +5 -0
- data/lib/motion-resource/associations.rb +94 -0
- data/lib/motion-resource/attributes.rb +37 -0
- data/lib/motion-resource/base.rb +51 -0
- data/lib/motion-resource/crud.rb +33 -0
- data/lib/motion-resource/find.rb +67 -0
- data/lib/motion-resource/requests.rb +64 -0
- data/lib/motion-resource/string.rb +23 -0
- data/lib/motion-resource/urls.rb +29 -0
- data/lib/motion-resource/version.rb +3 -0
- data/motion-resource.gemspec +20 -0
- data/spec/env.rb +41 -0
- data/spec/motion-resource/associations/belongs_to_spec.rb +116 -0
- data/spec/motion-resource/associations/has_many_spec.rb +128 -0
- data/spec/motion-resource/associations/has_one_spec.rb +69 -0
- data/spec/motion-resource/associations/scope_spec.rb +21 -0
- data/spec/motion-resource/attributes_spec.rb +64 -0
- data/spec/motion-resource/base_spec.rb +54 -0
- data/spec/motion-resource/crud_spec.rb +141 -0
- data/spec/motion-resource/find_spec.rb +90 -0
- data/spec/motion-resource/requests_spec.rb +119 -0
- data/spec/motion-resource/string_spec.rb +26 -0
- data/spec/motion-resource/urls_spec.rb +52 -0
- metadata +140 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# MotionResource
|
2
|
+
|
3
|
+
This is a library for using JSON APIs in RubyMotion apps. It is based on [RemoteModel](https://github.com/clayallsopp/remote_model), however it is an almost complete rewrite. Also, it is inspired by ActiveResource.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add MotionResource to your Gemfile, like this:
|
8
|
+
|
9
|
+
gem "motion-resource"
|
10
|
+
|
11
|
+
## Example
|
12
|
+
|
13
|
+
Consider this example for a fictional blog API.
|
14
|
+
|
15
|
+
class User < RemoteModule::RemoteModel
|
16
|
+
attr_accessor :id
|
17
|
+
|
18
|
+
has_many :posts
|
19
|
+
|
20
|
+
collection_url "users"
|
21
|
+
member_url "users/:id"
|
22
|
+
end
|
23
|
+
|
24
|
+
class Post < RemoteModule::RemoteModel
|
25
|
+
attr_accessor :id, :user_id, :title, :text
|
26
|
+
|
27
|
+
belongs_to :user
|
28
|
+
|
29
|
+
collection_url "users/:user_id/posts"
|
30
|
+
member_url "users/:user_id/posts/:id"
|
31
|
+
end
|
32
|
+
|
33
|
+
Now, we can access a user's posts like that:
|
34
|
+
|
35
|
+
User.find(1) do |user|
|
36
|
+
user.posts do |posts|
|
37
|
+
puts posts.inspect
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Note that the blocks are called asynchronously.
|
42
|
+
|
43
|
+
## Setup
|
44
|
+
|
45
|
+
You can configure every model separately; however you will most likely want to configure things like the root_url the same for every model:
|
46
|
+
|
47
|
+
MotionResource::Base.root_url = "http://localhost:3000/"
|
48
|
+
|
49
|
+
Don't forget the trailing '/' here!
|
50
|
+
|
51
|
+
# Forking
|
52
|
+
|
53
|
+
Feel free to fork and submit pull requests!
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
$:.unshift("/Library/RubyMotion/lib")
|
3
|
+
require 'motion/project'
|
4
|
+
require "bundler/gem_tasks"
|
5
|
+
Bundler.setup
|
6
|
+
Bundler.require
|
7
|
+
|
8
|
+
$:.unshift("./lib/")
|
9
|
+
require './lib/motion-resource'
|
10
|
+
|
11
|
+
Motion::Project::App.setup do |app|
|
12
|
+
# Use `rake config' to see complete project settings.
|
13
|
+
app.name = 'MotionResource'
|
14
|
+
end
|
data/app/app_delegate.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
$:.unshift("/Library/RubyMotion/lib")
|
3
|
+
require 'motion/project'
|
4
|
+
require 'motion-support'
|
5
|
+
require 'motion-resource'
|
6
|
+
|
7
|
+
Motion::Project::App.setup do |app|
|
8
|
+
# Use `rake config' to see complete project settings.
|
9
|
+
app.name = 'Colr'
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class AppDelegate
|
2
|
+
attr_reader :window
|
3
|
+
|
4
|
+
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
5
|
+
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
|
6
|
+
@window.rootViewController = MainNavigationController.alloc.init
|
7
|
+
@window.makeKeyAndVisible
|
8
|
+
true
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Color < MotionResource::Base
|
2
|
+
attr_accessor :hex
|
3
|
+
|
4
|
+
has_many :tags
|
5
|
+
|
6
|
+
scope :random, :url => 'json/colors/random/7'
|
7
|
+
|
8
|
+
def ui_color
|
9
|
+
pointer = Pointer.new(:uint)
|
10
|
+
scanner = NSScanner.scannerWithString(self.hex)
|
11
|
+
scanner.scanHexInt(pointer)
|
12
|
+
rgbValue = pointer[0]
|
13
|
+
return UIColor.colorWithRed(((rgbValue & 0xFF0000) >> 16)/255.0, green:((rgbValue & 0xFF00) >> 8)/255.0, blue:(rgbValue & 0xFF)/255.0, alpha:1.0)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class ColorViewController < UITableViewController
|
2
|
+
attr_accessor :colors
|
3
|
+
|
4
|
+
def init
|
5
|
+
self.colors = []
|
6
|
+
self.title = "7 random colors"
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def viewDidLoad
|
11
|
+
load_data
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def tableView(tableView, numberOfRowsInSection:section)
|
16
|
+
self.colors.size
|
17
|
+
end
|
18
|
+
|
19
|
+
def tableView(tableView, cellForRowAtIndexPath:indexPath)
|
20
|
+
cell = tableView.dequeueReusableCellWithIdentifier('Cell')
|
21
|
+
cell ||= UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:'Cell')
|
22
|
+
|
23
|
+
color = colors[indexPath.row]
|
24
|
+
cell.textLabel.text = color.hex
|
25
|
+
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator
|
26
|
+
cell
|
27
|
+
end
|
28
|
+
|
29
|
+
def tableView(tableView, willDisplayCell:cell, forRowAtIndexPath:indexPath)
|
30
|
+
color = colors[indexPath.row]
|
31
|
+
cell.backgroundColor = color.ui_color
|
32
|
+
end
|
33
|
+
|
34
|
+
def tableView(tableView, didSelectRowAtIndexPath:indexPath)
|
35
|
+
navigationController.pushViewController(TagsViewController.alloc.initWithColor(colors[indexPath.row]), animated:true)
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_data
|
39
|
+
Color.random do |results|
|
40
|
+
if results
|
41
|
+
self.colors = results
|
42
|
+
end
|
43
|
+
tableView.reloadData
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class TagsViewController < UITableViewController
|
2
|
+
attr_accessor :color
|
3
|
+
|
4
|
+
def initWithColor(color)
|
5
|
+
self.color = color
|
6
|
+
self.title = "Tags for color #{color.hex}"
|
7
|
+
init
|
8
|
+
end
|
9
|
+
|
10
|
+
def tableView(tableView, numberOfRowsInSection:section)
|
11
|
+
self.color.tags.size
|
12
|
+
end
|
13
|
+
|
14
|
+
def tableView(tableView, cellForRowAtIndexPath:indexPath)
|
15
|
+
cell = tableView.dequeueReusableCellWithIdentifier('Cell')
|
16
|
+
cell ||= UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:'Cell')
|
17
|
+
|
18
|
+
tag = color.tags[indexPath.row]
|
19
|
+
cell.textLabel.text = tag.name
|
20
|
+
cell.selectionStyle = UITableViewCellSelectionStyleNone
|
21
|
+
cell
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module MotionResource
|
2
|
+
class Base
|
3
|
+
class << self
|
4
|
+
def has_one(name)
|
5
|
+
define_method name do
|
6
|
+
instance_variable_get("@#{name}")
|
7
|
+
end
|
8
|
+
|
9
|
+
define_method "#{name}=" do |value|
|
10
|
+
klass = Object.const_get(name.to_s.classify)
|
11
|
+
value = klass.instantiate(value) if value.is_a?(Hash)
|
12
|
+
instance_variable_set("@#{name}", value)
|
13
|
+
end
|
14
|
+
|
15
|
+
define_method "reset_#{name}" do
|
16
|
+
instance_variable_set("@#{name}", nil)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_many(name, params = lambda { |o| Hash.new })
|
21
|
+
backwards_association = self.name.underscore
|
22
|
+
|
23
|
+
define_method name do |&block|
|
24
|
+
if block.nil?
|
25
|
+
instance_variable_get("@#{name}") || []
|
26
|
+
else
|
27
|
+
cached = instance_variable_get("@#{name}")
|
28
|
+
block.call(cached) and return if cached
|
29
|
+
|
30
|
+
Object.const_get(name.to_s.classify).find_all(params.call(self)) do |results|
|
31
|
+
if results && results.first && results.first.respond_to?("#{backwards_association}=")
|
32
|
+
results.each do |result|
|
33
|
+
result.send("#{backwards_association}=", self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
instance_variable_set("@#{name}", results)
|
37
|
+
block.call(results)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
define_method "#{name}=" do |array|
|
43
|
+
klass = Object.const_get(name.to_s.classify)
|
44
|
+
instance_variable_set("@#{name}", [])
|
45
|
+
|
46
|
+
array.each do |value|
|
47
|
+
value = klass.instantiate(value) if value.is_a?(Hash)
|
48
|
+
instance_variable_get("@#{name}") << value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
define_method "reset_#{name}" do
|
53
|
+
instance_variable_set("@#{name}", nil)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def belongs_to(name, params = lambda { |o| Hash.new })
|
58
|
+
define_method name do |&block|
|
59
|
+
if block.nil?
|
60
|
+
instance_variable_get("@#{name}")
|
61
|
+
else
|
62
|
+
cached = instance_variable_get("@#{name}")
|
63
|
+
block.call(cached) and return if cached
|
64
|
+
|
65
|
+
Object.const_get(name.to_s.classify).find(self.send("#{name}_id"), params.call(self)) do |result|
|
66
|
+
instance_variable_set("@#{name}", result)
|
67
|
+
block.call(result)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
define_method "#{name}=" do |value|
|
73
|
+
klass = Object.const_get(name.to_s.classify)
|
74
|
+
value = klass.instantiate(value) if value.is_a?(Hash)
|
75
|
+
instance_variable_set("@#{name}", value)
|
76
|
+
end
|
77
|
+
|
78
|
+
define_method "reset_#{name}" do
|
79
|
+
instance_variable_set("@#{name}", nil)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class << self
|
85
|
+
def scope(name, options = {})
|
86
|
+
custom_urls "#{name}_url" => options[:url] if options[:url]
|
87
|
+
|
88
|
+
metaclass.send(:define_method, name) do |&block|
|
89
|
+
fetch_collection(send("#{name}_url"), &block)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module MotionResource
|
2
|
+
class Base
|
3
|
+
class_inheritable_array :attributes
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def attribute(*fields)
|
7
|
+
attr_reader *fields
|
8
|
+
fields.each do |field|
|
9
|
+
define_method "#{field}=" do |value|
|
10
|
+
if value.is_a?(Hash) || value.is_a?(Array)
|
11
|
+
instance_variable_set("@#{field}", value.dup)
|
12
|
+
else
|
13
|
+
instance_variable_set("@#{field}", value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
self.attributes += fields
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_attributes(params = {})
|
22
|
+
attributes = self.methods - Object.methods
|
23
|
+
params.each do |key, value|
|
24
|
+
if attributes.member?((key.to_s + "=:").to_sym)
|
25
|
+
self.send((key.to_s + "=:").to_sym, value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes
|
31
|
+
self.class.attributes.inject({}) do |hash, attr|
|
32
|
+
hash[attr] = send(attr)
|
33
|
+
hash
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module MotionResource
|
2
|
+
class Base
|
3
|
+
attr_accessor :id
|
4
|
+
|
5
|
+
def initialize(params = {})
|
6
|
+
@new_record = true
|
7
|
+
update_attributes(params)
|
8
|
+
end
|
9
|
+
|
10
|
+
def new_record?
|
11
|
+
@new_record
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def instantiate(json)
|
16
|
+
raise ArgumentError, "No :id parameter given for #{self.name}.instantiate" unless json[:id]
|
17
|
+
|
18
|
+
klass = if json[:type]
|
19
|
+
begin
|
20
|
+
Object.const_get(json[:type].to_s)
|
21
|
+
rescue NameError
|
22
|
+
self
|
23
|
+
end
|
24
|
+
else
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
if result = klass.recall(json[:id])
|
29
|
+
result.update_attributes(json)
|
30
|
+
else
|
31
|
+
result = klass.new(json)
|
32
|
+
klass.remember(result.id, result)
|
33
|
+
end
|
34
|
+
result.send(:instance_variable_set, "@new_record", false)
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def identity_map
|
39
|
+
@identity_map ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def remember(id, value)
|
43
|
+
identity_map[id] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
def recall(id)
|
47
|
+
identity_map[id]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module MotionResource
|
2
|
+
class Base
|
3
|
+
def save(&block)
|
4
|
+
@new_record ? create(&block) : update(&block)
|
5
|
+
end
|
6
|
+
|
7
|
+
def update(&block)
|
8
|
+
self.class.put(member_url, :payload => { self.class.name.underscore => attributes }) do |response, json|
|
9
|
+
block.call json.blank? ? nil : self.class.instantiate(json) if block
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def create(&block)
|
14
|
+
# weird heisenbug: Specs crash without that line :(
|
15
|
+
dummy = self
|
16
|
+
self.class.post(collection_url, :payload => { self.class.name.underscore => attributes }) do |response, json|
|
17
|
+
block.call json.blank? ? nil : self.class.instantiate(json) if block
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy(&block)
|
22
|
+
self.class.delete(member_url) do |response, json|
|
23
|
+
block.call json.blank? ? nil : self.class.instantiate(json) if block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def reload(&block)
|
28
|
+
self.class.get(member_url) do |response, json|
|
29
|
+
block.call json.blank? ? nil : self.class.instantiate(json) if block
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|