lissio 0.1.0.beta1

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.
@@ -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
+