lissio 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,204 @@
1
+ #--
2
+ # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
3
+ # Version 2, December 2004
4
+ #
5
+ # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
6
+ # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
7
+ #
8
+ # 0. You just DO WHAT THE FUCK YOU WANT TO.
9
+ #++
10
+
11
+ require 'json'
12
+ require 'forwardable'
13
+
14
+ module Lissio
15
+
16
+ class Model
17
+ class Property
18
+ attr_reader :name, :as
19
+
20
+ def initialize(name, options)
21
+ @name = name
22
+ @default = options[:default]
23
+ @primary = options[:primary] || false
24
+ @as = options[:as]
25
+ end
26
+
27
+ def primary?
28
+ @primary
29
+ end
30
+
31
+ def default
32
+ if Proc === @default && @default.lambda?
33
+ @default.call
34
+ else
35
+ @default
36
+ end
37
+ end
38
+
39
+ def new(data)
40
+ return default if data.nil?
41
+ return data if !@as || @as === data
42
+
43
+ if @as.ancestors.include?(Model)
44
+ if @as.primary.as === data
45
+ return data
46
+ else
47
+ return @as.new(*data)
48
+ end
49
+ end
50
+
51
+ case
52
+ when @as == Boolean then !!data
53
+ when @as == Array then Array(data)
54
+ when @as == String then data.to_s
55
+ when @as == Symbol then data.to_sym
56
+ when @as == Integer then data.to_i
57
+ when @as == Float then data.to_f
58
+ when @as == Time then Time.parse(data)
59
+ when Proc === @as then @as.call(data)
60
+ else @as.new(*data)
61
+ end
62
+ end
63
+
64
+ def define(klass)
65
+ name = @name
66
+ as = @as
67
+
68
+ if Class === @as && @as.ancestors.include?(Model)
69
+ klass.define_method name do
70
+ if id = instance_variable_get("@#{name}")
71
+ as.fetch(id)
72
+ end
73
+ end
74
+
75
+ klass.define_method "#{name}=" do |value|
76
+ if instance_variable_get("@#{name}") != value.id!
77
+ @changed << name
78
+
79
+ instance_variable_set "@#{name}", value.id!
80
+ end
81
+ end
82
+ else
83
+ klass.define_method name do
84
+ instance_variable_get "@#{name}"
85
+ end
86
+
87
+ klass.define_method "#{name}=" do |value|
88
+ if instance_variable_get("@#{name}") != value
89
+ @changed << name
90
+
91
+ instance_variable_set "@#{name}", value
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def self.inherited(klass)
99
+ klass.instance_eval {
100
+ @properties = {}
101
+ }
102
+
103
+ return if self == Model
104
+
105
+ klass.instance_eval {
106
+ def self.properties
107
+ superclass.properties.merge @properties
108
+ end
109
+ }
110
+ end
111
+
112
+ def self.adapter(klass = nil, *args, &block)
113
+ if klass
114
+ @adapter.uninstall if @adapter
115
+
116
+ @adapter = klass.new(self, *args, &block)
117
+ @adapter.install
118
+ else
119
+ @adapter
120
+ end
121
+ end
122
+
123
+ def self.for(klass, *args, &block)
124
+ Class.new(self) {
125
+ adapter(klass, *args, &block)
126
+ }
127
+ end
128
+
129
+ def self.properties
130
+ @properties
131
+ end
132
+
133
+ def self.property(name, options = {})
134
+ # @properties is accessed directly to allow proper inheritance
135
+ @properties[name] = Property.new(name, options).tap {|property|
136
+ property.define(self)
137
+
138
+ if property.primary?
139
+ @primary = property
140
+ end
141
+ }
142
+ end
143
+
144
+ def self.primary
145
+ @primary
146
+ end
147
+
148
+ extend Forwardable
149
+ def_delegators :class, :adapter, :properties
150
+
151
+ attr_reader :fetched_with, :changed
152
+
153
+ def initialize(data = nil, *fetched_with)
154
+ @fetched_with = fetched_with
155
+ @changed = []
156
+
157
+ if data
158
+ properties.each {|name, property|
159
+ instance_variable_set "@#{name}", property.new(data[name])
160
+ }
161
+ end
162
+ end
163
+
164
+ def changed?
165
+ not @changed.empty?
166
+ end
167
+
168
+ def id!
169
+ name, = properties.find {|_, property|
170
+ property.primary?
171
+ }
172
+
173
+ instance_variable_get "@#{name || :id}"
174
+ end
175
+
176
+ def to_h
177
+ Hash[properties.map {|name, _|
178
+ [name, instance_variable_get("@#{name}")]
179
+ }]
180
+ end
181
+
182
+ def as_json
183
+ hash = to_h
184
+ hash[JSON.create_id] = self.class.name
185
+
186
+ hash
187
+ end
188
+
189
+ def to_json
190
+ as_json.to_json
191
+ end
192
+
193
+ def self.json_create(data)
194
+ new(data)
195
+ end
196
+
197
+ def inspect
198
+ "#<#{self.class.name}: #{properties.map {|name, _|
199
+ "#{name}=#{instance_variable_get("@#{name}").inspect}"
200
+ }.join(' ')}>"
201
+ end
202
+ end
203
+
204
+ end
@@ -0,0 +1,164 @@
1
+ #--
2
+ # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
3
+ # Version 2, December 2004
4
+ #
5
+ # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
6
+ # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
7
+ #
8
+ # 0. You just DO WHAT THE FUCK YOU WANT TO.
9
+ #++
10
+
11
+ require 'browser/history'
12
+
13
+ module Lissio
14
+
15
+ class Router
16
+ attr_reader :routes, :options
17
+
18
+ def initialize(options = {}, &block)
19
+ @routes = []
20
+ @options = options
21
+ @location = $document.location
22
+ @initial = @location.uri
23
+ @history = $window.history
24
+
25
+ @change = if fragment?
26
+ $window.on 'hash:change' do
27
+ update
28
+ end
29
+ else
30
+ $window.on 'pop:state' do |e|
31
+ if @initial == @location.uri
32
+ @initial = nil
33
+ break
34
+ else
35
+ @initial = nil
36
+ end
37
+
38
+ update
39
+ end
40
+ end
41
+
42
+ if block.arity == 0
43
+ instance_exec(&block)
44
+ else
45
+ block.call(self)
46
+ end if block
47
+ end
48
+
49
+ def fragment?
50
+ @options[:fragment] != false || !`window.history.pushState`
51
+ end
52
+
53
+ def html5!
54
+ return unless fragment?
55
+
56
+ @options[:fragment] = false
57
+
58
+ @change.off
59
+ @change = $window.on 'pop:state' do
60
+ update
61
+ end
62
+ end
63
+
64
+ def fragment!
65
+ return if fragment?
66
+
67
+ @options[:fragment] = true
68
+
69
+ @change.off
70
+ @change = $window.on 'hash:change' do
71
+ update
72
+ end
73
+ end
74
+
75
+ def path
76
+ if fragment?
77
+ if @location.fragment.empty?
78
+ "/"
79
+ else
80
+ @location.fragment.sub(/^#*/, '')
81
+ end
82
+ else
83
+ @location.path
84
+ end
85
+ end
86
+
87
+ def route(path, &block)
88
+ Route.new(path, &block).tap {|route|
89
+ @routes << route
90
+ }
91
+ end
92
+
93
+ def update
94
+ match path
95
+ end
96
+
97
+ def navigate(path)
98
+ if path == self.path
99
+ return update
100
+ end
101
+
102
+ if fragment?
103
+ @location.fragment = "##{path}"
104
+ else
105
+ @history.push(path)
106
+ update
107
+ end
108
+ end
109
+
110
+ private
111
+ def match(path)
112
+ @routes.find {|route|
113
+ route.match path
114
+ }
115
+ end
116
+
117
+ class Route
118
+ # Regexp for matching named params in path
119
+ NAME = /:(\w+)/
120
+
121
+ # Regexp for matching named splats in path
122
+ SPLAT = /\\\*(\w+)/
123
+
124
+ attr_reader :names
125
+
126
+ def initialize(pattern, &block)
127
+ @names = []
128
+ @block = block
129
+
130
+ pattern = Regexp.escape(pattern)
131
+
132
+ pattern.scan(NAME) {|name,|
133
+ @names << name
134
+ }
135
+
136
+ pattern.scan(SPLAT) {|name,|
137
+ @names << name
138
+ }
139
+
140
+ pattern = pattern.gsub(NAME, "([^\\/]+)").
141
+ gsub(SPLAT, "(.+?)")
142
+
143
+ @regexp = Regexp.new "^#{pattern}$"
144
+ end
145
+
146
+ def match(path)
147
+ if match = @regexp.match(path)
148
+ params = {}
149
+
150
+ @names.each_with_index {|name, index|
151
+ params[name] = match[index + 1]
152
+ }
153
+
154
+ @block.call params if @block
155
+
156
+ true
157
+ else
158
+ false
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ end
@@ -0,0 +1,3 @@
1
+ module Lissio
2
+ VERSION = '0.1.0.beta1'
3
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lissio::Router::Route do
4
+ describe "#initialize" do
5
+ let(:route) { Lissio::Router::Route }
6
+
7
+ it "creates a regexp from the given pattern" do
8
+ route.new('foo').instance_variable_get(:@regexp).should eq(/^foo$/)
9
+ end
10
+
11
+ it "escapes slashes in the pattern" do
12
+ route.new('/foo/bar/').instance_variable_get(:@regexp).should eq(/^\/foo\/bar\/$/)
13
+ end
14
+
15
+ it "finds named params in pattern" do
16
+ r = route.new('/foo/:bar')
17
+ r.names.should eq(['bar'])
18
+
19
+ p = route.new('/:woosh/:kapow')
20
+ p.names.should eq(['woosh', 'kapow'])
21
+ end
22
+
23
+ it "finds splatted params in pattern" do
24
+ route.new('/foo/*baz').names.should eq(['baz'])
25
+ end
26
+
27
+ it "produces a regexp to match given pattern" do
28
+ route.new('/foo').instance_variable_get(:@regexp).match('/bar').should be_nil
29
+ route.new('/foo').instance_variable_get(:@regexp).match('/foo').should be_kind_of(MatchData)
30
+ end
31
+ end
32
+
33
+ describe "#match" do
34
+ let(:route) { Lissio::Router::Route }
35
+
36
+ it "returns false for a non matching route" do
37
+ route.new('/foo').match('/a/b/c').should be_false
38
+ end
39
+
40
+ it "returns true for a matching route" do
41
+ route.new('/foo').match('/foo').should be_true
42
+ end
43
+
44
+ it "calls block given to #initialize on matching a route" do
45
+ called = false
46
+ route.new('/foo') { called = true }.match('/foo')
47
+ called.should be_true
48
+ end
49
+
50
+ it "calls handler with an empty hash for a simple route" do
51
+ route.new('/foo') { |params| params.should eq({}) }.match('/foo')
52
+ end
53
+
54
+ it "returns a hash of named params for matching route" do
55
+ route.new('/foo/:first') { |params|
56
+ params.should eq({'first' => '123' })
57
+ }.match('/foo/123')
58
+
59
+ route.new('/:first/:second') { |params|
60
+ params.should eq({ 'first' => 'woosh', 'second' => 'kapow' })
61
+ }.match('/woosh/kapow')
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lissio::Router do
4
+ let(:router) { Lissio::Router.new }
5
+
6
+ def set_native_hash(hash)
7
+ `window.location.hash = #{hash}`
8
+ end
9
+
10
+ describe "#update" do
11
+ it "should update Router.path" do
12
+ set_native_hash '#/hello/world'
13
+ router.update
14
+
15
+ router.path.should eq('/hello/world')
16
+ end
17
+
18
+ it "calls #match with the new @path" do
19
+ set_native_hash '#/foo/bar'
20
+ called = nil
21
+
22
+ router.define_singleton_method(:match) { |m| called = m }
23
+ called.should be_nil
24
+
25
+ router.update
26
+ called.should eq("/foo/bar")
27
+ end
28
+ end
29
+
30
+ describe "#route" do
31
+ it "should add a route" do
32
+ router.route('/users') {}
33
+ router.routes.size.should eq(1)
34
+
35
+ router.route('/hosts') {}
36
+ router.routes.size.should eq(2)
37
+ end
38
+ end
39
+
40
+ describe "#match" do
41
+ it "returns nil when no routes on router" do
42
+ router.match('/foo').should be_nil
43
+ end
44
+
45
+ it "returns a matching route for the path" do
46
+ a = router.route('/foo') {}
47
+ b = router.route('/bar') {}
48
+
49
+ router.match('/foo').should eq(a)
50
+ router.match('/bar').should eq(b)
51
+ end
52
+
53
+ it "returns nil when there are no matching routes" do
54
+ router.route('/woosh') {}
55
+ router.route('/kapow') {}
56
+
57
+ router.match('/ping').should be_nil
58
+ end
59
+
60
+ it "calls handler of matching route" do
61
+ out = []
62
+ router.route('/foo') { out << :foo }
63
+ router.route('/bar') { out << :bar }
64
+
65
+ router.match('/foo')
66
+ out.should eq([:foo])
67
+
68
+ router.match('/bar')
69
+ out.should eq([:foo, :bar])
70
+
71
+ router.match('/eek')
72
+ out.should eq([:foo, :bar])
73
+ end
74
+
75
+ it "works with / too" do
76
+ out = []
77
+ router.route('/') { out << :index }
78
+
79
+ $global.location.hash = ""
80
+ router.update
81
+
82
+ $global.location.hash = "#/"
83
+ router.update
84
+
85
+ out.should == [:index, :index]
86
+ end
87
+ end
88
+
89
+ describe "#navigate" do
90
+ it "should update location.hash" do
91
+ router.navigate "foo"
92
+ $global.location.hash.should eq("#foo")
93
+ end
94
+
95
+ it "triggers the route matchers" do
96
+ called = false
97
+ router.route("/foo") { called = true }
98
+
99
+ router.navigate("/bar")
100
+ router.update
101
+ called.should be_false
102
+
103
+ router.navigate("/foo")
104
+ router.update
105
+ called.should be_true
106
+ end
107
+ end
108
+ end
109
+