clearwater 0.1.2 → 0.2.0
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.
- checksums.yaml +4 -4
- data/lib/clearwater/version.rb +1 -1
- data/opal/clearwater/application.rb +31 -35
- data/opal/clearwater/cached_render.rb +16 -7
- data/opal/clearwater/component.rb +5 -22
- data/opal/clearwater/virtual_dom.rb +12 -4
- data/spec/clearwater/application_spec.rb +45 -0
- data/spec/clearwater/cached_render_spec.rb +46 -0
- data/spec/clearwater/router_spec.rb +51 -0
- data/spec/component_spec.rb +67 -0
- metadata +14 -9
- data/opal/clearwater/api_client.rb +0 -90
- data/opal/clearwater/cgi.rb +0 -14
- data/opal/clearwater/store.rb +0 -99
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d75fcebb1932de023f32ad43613c30907bfed76
|
4
|
+
data.tar.gz: 1b0b6fba9eef8bf9539f1c7cc454af550fb40ff2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70fac50714fb26e2600bf9e23d3ebaee2350b1500cfc21ec36265e0f8d30376e0abb7cabad394f92e5346f51211ec960060fc19d6257993a784c12365d5abf4c
|
7
|
+
data.tar.gz: 03a1ea608623bb899094124e71cae1708fcd40f4940d52d06de73fe570f26af8279d0e33bac731f995ac0153f031ac52c0362c0ed6ffd2e23761abddd4137197
|
data/lib/clearwater/version.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require 'clearwater/router'
|
2
2
|
require 'clearwater/application_registry'
|
3
|
+
require 'browser'
|
3
4
|
require 'browser/delay'
|
4
|
-
require '
|
5
|
+
require 'native'
|
6
|
+
require 'browser/event'
|
7
|
+
require 'browser/animation_frame'
|
5
8
|
|
6
9
|
module Clearwater
|
7
10
|
class Application
|
8
|
-
RENDER_FPS = 60
|
9
11
|
AppRegistry = ApplicationRegistry.new
|
10
12
|
|
11
13
|
attr_reader :store, :router, :component, :api_client, :on_render
|
@@ -15,17 +17,17 @@ module Clearwater
|
|
15
17
|
end
|
16
18
|
|
17
19
|
def initialize options={}
|
18
|
-
@store = options.fetch(:store) { nil }
|
19
20
|
@router = options.fetch(:router) { Router.new }
|
20
21
|
@component = options.fetch(:component) { nil }
|
21
|
-
@api_client = options.fetch(:api_client) { nil }
|
22
22
|
@element = options.fetch(:element) { nil }
|
23
|
+
@document = options.fetch(:document) { $document }
|
24
|
+
@window = options.fetch(:window) { $window }
|
23
25
|
@on_render = []
|
24
26
|
|
25
27
|
router.application = self
|
26
|
-
component.router = router
|
28
|
+
component.router = router if component
|
27
29
|
|
28
|
-
|
30
|
+
@document.on 'visibilitychange' do
|
29
31
|
if @render_on_visibility_change
|
30
32
|
@render_on_visibility_change = false
|
31
33
|
render
|
@@ -33,16 +35,16 @@ module Clearwater
|
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
36
|
-
def call
|
38
|
+
def call &block
|
37
39
|
AppRegistry << self
|
38
|
-
render_current_url
|
40
|
+
render_current_url &block
|
39
41
|
watch_url
|
40
42
|
end
|
41
43
|
|
42
44
|
def watch_url
|
43
45
|
unless @watching_url
|
44
46
|
@watching_url = true
|
45
|
-
|
47
|
+
@window.on('popstate') { render_current_url }
|
46
48
|
end
|
47
49
|
end
|
48
50
|
|
@@ -53,6 +55,8 @@ module Clearwater
|
|
53
55
|
|
54
56
|
def render &block
|
55
57
|
on_render << block if block
|
58
|
+
return if @will_render
|
59
|
+
@will_render = true
|
56
60
|
|
57
61
|
# If the app isn't being shown, wait to render until it is.
|
58
62
|
if `document.hidden`
|
@@ -60,15 +64,15 @@ module Clearwater
|
|
60
64
|
return
|
61
65
|
end
|
62
66
|
|
63
|
-
|
67
|
+
animation_frame { perform_render }
|
64
68
|
|
65
69
|
nil
|
66
70
|
end
|
67
71
|
|
68
72
|
def element
|
69
73
|
@element ||= begin
|
70
|
-
if
|
71
|
-
|
74
|
+
if @document.body
|
75
|
+
@document.body
|
72
76
|
else
|
73
77
|
nil
|
74
78
|
end
|
@@ -77,10 +81,10 @@ module Clearwater
|
|
77
81
|
|
78
82
|
def benchmark message
|
79
83
|
if debug?
|
80
|
-
start =
|
84
|
+
start = `performance.now()`
|
81
85
|
result = yield
|
82
|
-
finish =
|
83
|
-
puts "#{message} in #{(finish - start)
|
86
|
+
finish = `performance.now()`
|
87
|
+
puts "#{message} in #{(finish - start).round(3)}ms"
|
84
88
|
result
|
85
89
|
else
|
86
90
|
yield
|
@@ -88,19 +92,11 @@ module Clearwater
|
|
88
92
|
end
|
89
93
|
|
90
94
|
def debug?
|
91
|
-
|
95
|
+
!!@debug
|
92
96
|
end
|
93
97
|
|
94
|
-
def
|
95
|
-
|
96
|
-
unless @next_render
|
97
|
-
@next_render = last_render + time_between_renders
|
98
|
-
now = Time.now
|
99
|
-
after [@next_render - now, time_between_renders].max do
|
100
|
-
perform_render
|
101
|
-
@next_render = nil
|
102
|
-
end
|
103
|
-
end
|
98
|
+
def debug!
|
99
|
+
@debug = true
|
104
100
|
end
|
105
101
|
|
106
102
|
def perform_render
|
@@ -108,21 +104,21 @@ module Clearwater
|
|
108
104
|
raise TypeError, "Cannot render to a non-existent element. Make sure the document ready event has been triggered before invoking the application."
|
109
105
|
end
|
110
106
|
|
111
|
-
@_virtual_dom ||= VirtualDOM::Document.new(element)
|
112
|
-
|
113
107
|
rendered = benchmark('Generated virtual DOM') { component.render }
|
114
|
-
benchmark('Rendered to actual DOM') {
|
108
|
+
benchmark('Rendered to actual DOM') { virtual_dom.render rendered }
|
115
109
|
@last_render = Time.now
|
116
|
-
|
117
|
-
|
110
|
+
@will_render = false
|
111
|
+
run_callbacks
|
112
|
+
nil
|
118
113
|
end
|
119
114
|
|
120
|
-
def
|
121
|
-
@
|
115
|
+
def virtual_dom
|
116
|
+
@virtual_dom ||= VirtualDOM::Document.new(element)
|
122
117
|
end
|
123
118
|
|
124
|
-
def
|
125
|
-
|
119
|
+
def run_callbacks
|
120
|
+
on_render.each(&:call)
|
121
|
+
on_render.clear
|
126
122
|
end
|
127
123
|
end
|
128
124
|
end
|
@@ -1,14 +1,23 @@
|
|
1
1
|
module Clearwater
|
2
2
|
module CachedRender
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
|
4
|
+
def self.included base
|
5
|
+
%x{
|
6
|
+
Opal.defn(base, 'type', 'Thunk');
|
7
|
+
Opal.defn(base, 'render', function(prev) {
|
8
|
+
var self = this;
|
9
|
+
var should_render;
|
10
|
+
|
11
|
+
if(prev && prev.vnode && #{!should_render?(`prev`)}) {
|
12
|
+
return prev.vnode;
|
13
|
+
} else {
|
14
|
+
return #{sanitize_content(render)};
|
15
|
+
}
|
16
|
+
});
|
17
|
+
}
|
9
18
|
end
|
10
19
|
|
11
|
-
def should_render?
|
20
|
+
def should_render? _
|
12
21
|
false
|
13
22
|
end
|
14
23
|
end
|
@@ -1,23 +1,10 @@
|
|
1
1
|
require 'clearwater/virtual_dom'
|
2
|
-
require '
|
2
|
+
require 'browser'
|
3
3
|
|
4
4
|
module Clearwater
|
5
5
|
module Component
|
6
6
|
attr_accessor :router, :outlet
|
7
7
|
|
8
|
-
def self.included(klass)
|
9
|
-
def klass.attributes(*attrs)
|
10
|
-
attrs.each do |attr|
|
11
|
-
ivar = "@#{attr}"
|
12
|
-
define_method(attr) { instance_variable_get(ivar) }
|
13
|
-
define_method("#{attr}=") do |value|
|
14
|
-
instance_variable_set ivar, value
|
15
|
-
call
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
8
|
def render
|
22
9
|
end
|
23
10
|
|
@@ -142,7 +129,7 @@ module Clearwater
|
|
142
129
|
end
|
143
130
|
|
144
131
|
def tag tag_name, attributes=nil, content=nil
|
145
|
-
if !(`attributes
|
132
|
+
if !(`attributes.$$is_hash || attributes === #{nil}`)
|
146
133
|
content = attributes
|
147
134
|
attributes = nil
|
148
135
|
end
|
@@ -158,10 +145,6 @@ module Clearwater
|
|
158
145
|
router.params_for_path(router.current_path)
|
159
146
|
end
|
160
147
|
|
161
|
-
def param(key)
|
162
|
-
params[key]
|
163
|
-
end
|
164
|
-
|
165
148
|
def sanitize_attributes attributes
|
166
149
|
return attributes unless attributes.is_a? Hash
|
167
150
|
|
@@ -189,7 +172,7 @@ module Clearwater
|
|
189
172
|
|
190
173
|
def sanitize_content content
|
191
174
|
%x{
|
192
|
-
if(content.$$class) {
|
175
|
+
if(content && content.$$class) {
|
193
176
|
if(content.$$class === Opal.Array) {
|
194
177
|
return #{content.map { |c| `self.$sanitize_content(c)` }};
|
195
178
|
} else if(content === Opal.nil) {
|
@@ -198,8 +181,8 @@ module Clearwater
|
|
198
181
|
var cached_render = content.$cached_render;
|
199
182
|
var render = content.$render;
|
200
183
|
|
201
|
-
if(
|
202
|
-
return content
|
184
|
+
if(content.type === 'Thunk' && typeof(content.render) === 'function') {
|
185
|
+
return content;
|
203
186
|
} else if(render && !render.$$stub) {
|
204
187
|
return self.$sanitize_content(content.$render());
|
205
188
|
} else {
|
@@ -2,12 +2,16 @@ require 'browser'
|
|
2
2
|
require 'clearwater/virtual_dom/js/virtual_dom.js'
|
3
3
|
|
4
4
|
module VirtualDOM
|
5
|
-
def self.node(tag_name, attributes, content)
|
5
|
+
def self.node(tag_name, attributes=nil, content=nil)
|
6
6
|
content = sanitize_content(content)
|
7
7
|
attributes = HashUtils.camelize_keys(attributes).to_n
|
8
8
|
`virtualDom.h(tag_name, attributes, content)`
|
9
9
|
end
|
10
10
|
|
11
|
+
def self.create_element(node)
|
12
|
+
`virtualDom.create(node)`
|
13
|
+
end
|
14
|
+
|
11
15
|
def self.diff first, second
|
12
16
|
`virtualDom.diff(first, second)`
|
13
17
|
end
|
@@ -18,8 +22,8 @@ module VirtualDOM
|
|
18
22
|
|
19
23
|
def self.sanitize_content content
|
20
24
|
%x{
|
21
|
-
if(content === Opal.nil) return
|
22
|
-
if(content.$$
|
25
|
+
if(content === Opal.nil || content === undefined) return null;
|
26
|
+
if(content.$$is_array)
|
23
27
|
return #{content.map!{ |c| sanitize_content c }};
|
24
28
|
return content;
|
25
29
|
}
|
@@ -54,7 +58,11 @@ module VirtualDOM
|
|
54
58
|
|
55
59
|
module StringUtils
|
56
60
|
def self.camelize string
|
57
|
-
|
61
|
+
%x{
|
62
|
+
return string.replace(/_(\w)/g, function(full_match, character_match) {
|
63
|
+
return character_match.toUpperCase();
|
64
|
+
})
|
65
|
+
}
|
58
66
|
end
|
59
67
|
end
|
60
68
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'clearwater'
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
RSpec.describe Application do
|
5
|
+
let(:app) {
|
6
|
+
Application.new(
|
7
|
+
component: component,
|
8
|
+
element: element,
|
9
|
+
)
|
10
|
+
}
|
11
|
+
let(:component) {
|
12
|
+
Class.new do
|
13
|
+
include Clearwater::Component
|
14
|
+
|
15
|
+
def render
|
16
|
+
h1('Hello world')
|
17
|
+
end
|
18
|
+
end.new
|
19
|
+
}
|
20
|
+
let(:element) { $document.create_element('div') }
|
21
|
+
|
22
|
+
it 'renders to the specified element' do
|
23
|
+
app.perform_render
|
24
|
+
|
25
|
+
expect(element.inner_html).to eq '<h1>Hello world</h1>'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'calls queued blocks after rendering' do
|
29
|
+
i = 1
|
30
|
+
app.on_render << proc { i += 1 }
|
31
|
+
app.on_render << proc { i += 1 }
|
32
|
+
|
33
|
+
app.perform_render
|
34
|
+
expect(i).to eq 3
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'empties the block queue after rendering' do
|
38
|
+
app.on_render << proc { }
|
39
|
+
|
40
|
+
app.perform_render
|
41
|
+
|
42
|
+
expect(app.on_render).to be_empty
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'clearwater/component'
|
2
|
+
require 'clearwater/cached_render'
|
3
|
+
|
4
|
+
module Clearwater
|
5
|
+
describe CachedRender do
|
6
|
+
let(:component_class) {
|
7
|
+
Class.new do
|
8
|
+
include Clearwater::Component
|
9
|
+
include Clearwater::CachedRender
|
10
|
+
|
11
|
+
def initialize value
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def render
|
16
|
+
@value.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
}
|
20
|
+
let(:value) { double }
|
21
|
+
let(:component) { component_class.new(value) }
|
22
|
+
|
23
|
+
it 'memoizes the return value of render' do
|
24
|
+
component = component()
|
25
|
+
|
26
|
+
expect(value).to receive(:to_s)
|
27
|
+
%x{ component.render(component) }
|
28
|
+
|
29
|
+
component.instance_exec { @vnode = VirtualDOM.node('div', 'howdy') }
|
30
|
+
expect(value).not_to receive(:to_s)
|
31
|
+
|
32
|
+
2.times { `component.render(component)` }
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'uses should_render? to determine whether to call render again' do
|
36
|
+
component = component()
|
37
|
+
def component.should_render?
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
expect(value).to receive(:to_s).twice
|
42
|
+
|
43
|
+
2.times { `component.render(component)` }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'clearwater/router'
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
RSpec.describe Router do
|
5
|
+
let(:component_class) {
|
6
|
+
Class.new do
|
7
|
+
include Clearwater::Component
|
8
|
+
end
|
9
|
+
}
|
10
|
+
let(:routed_component) { component_class.new }
|
11
|
+
let!(:router) {
|
12
|
+
component = routed_component
|
13
|
+
Router.new do
|
14
|
+
route 'articles' => component do
|
15
|
+
route ':article_id' => component
|
16
|
+
end
|
17
|
+
end
|
18
|
+
}
|
19
|
+
|
20
|
+
it 'sets the router for a routed component' do
|
21
|
+
expect(routed_component.router).to be router
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'gets the params for a given path' do
|
25
|
+
expect(router.params_for_path('/articles/foo')).to eq article_id: 'foo'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'gets the components for a given path' do
|
29
|
+
expect(router.targets_for_path('/articles/1')).to eq [
|
30
|
+
routed_component, routed_component
|
31
|
+
]
|
32
|
+
|
33
|
+
expect(router.targets_for_path('/articles')).to eq [routed_component]
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'gets the current path' do
|
37
|
+
location = { pathname: '/foo' }
|
38
|
+
router = Router.new(location: location)
|
39
|
+
|
40
|
+
expect(router.current_path).to eq '/foo'
|
41
|
+
|
42
|
+
location[:pathname] = '/bar'
|
43
|
+
|
44
|
+
expect(router.current_path).to eq '/bar'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'gets the params from the path' do
|
48
|
+
expect(router.params_for_path('/articles/123')).to eq({ article_id: '123' })
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'clearwater/component'
|
2
|
+
|
3
|
+
# When running specs from the CLI, it doesn't define this class
|
4
|
+
unless defined? Browser::Event
|
5
|
+
module Browser
|
6
|
+
class Event
|
7
|
+
def initialize event
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Clearwater
|
14
|
+
RSpec.describe Component do
|
15
|
+
let(:component_class) {
|
16
|
+
Class.new do
|
17
|
+
include Clearwater::Component
|
18
|
+
end
|
19
|
+
}
|
20
|
+
let(:component) { component_class.new }
|
21
|
+
|
22
|
+
it 'provides a default render method' do
|
23
|
+
expect(component.render).to be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
Component::HTML_TAGS.each do |tag|
|
27
|
+
it "provides helpers for `#{tag}` elements" do
|
28
|
+
expect(`#{component.send(tag)}.tagName`).to eq tag.upcase
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sanitizes element attributes' do
|
33
|
+
attributes = component.sanitize_attributes({
|
34
|
+
class: 'foo',
|
35
|
+
onclick: proc { |event| expect(event).to be_a Browser::Event },
|
36
|
+
})
|
37
|
+
|
38
|
+
# Renames :class to :class_name
|
39
|
+
expect(attributes[:class_name]).to eq 'foo'
|
40
|
+
|
41
|
+
# Wraps yielded events in a Browser::Event
|
42
|
+
attributes[:onclick].call(`document.createEvent('MouseEvent')`)
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'sanitizing content' do
|
46
|
+
it 'sanitizes components by calling `render`' do
|
47
|
+
allow(component).to receive(:render) { 'foo' }
|
48
|
+
expect(component.sanitize_content(component)).to eq 'foo'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'sanitizes arrays by sanitizing each element' do
|
52
|
+
allow(component).to receive(:render) { 'foo' }
|
53
|
+
expect(component.sanitize_content([component, nil, 1])).to eq ['foo', '', 1]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'retrieves params from the router' do
|
58
|
+
router = double('Router')
|
59
|
+
params = { article_id: 123 }
|
60
|
+
allow(router).to receive(:params_for_path) { params }
|
61
|
+
allow(router).to receive(:current_path) { '/articles/123' }
|
62
|
+
component.router = router
|
63
|
+
|
64
|
+
expect(component.params).to eq params
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clearwater
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamie Gaskins
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: opal
|
@@ -53,19 +53,19 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '1.3'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name: rspec
|
56
|
+
name: opal-rspec
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 0.5.0.beta2
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 0.5.0.beta2
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -132,19 +132,20 @@ files:
|
|
132
132
|
- lib/clearwater.rb
|
133
133
|
- lib/clearwater/version.rb
|
134
134
|
- opal/clearwater.rb
|
135
|
-
- opal/clearwater/api_client.rb
|
136
135
|
- opal/clearwater/application.rb
|
137
136
|
- opal/clearwater/application_registry.rb
|
138
137
|
- opal/clearwater/cached_render.rb
|
139
|
-
- opal/clearwater/cgi.rb
|
140
138
|
- opal/clearwater/component.rb
|
141
139
|
- opal/clearwater/link.rb
|
142
140
|
- opal/clearwater/router.rb
|
143
141
|
- opal/clearwater/router/route.rb
|
144
142
|
- opal/clearwater/router/route_collection.rb
|
145
|
-
- opal/clearwater/store.rb
|
146
143
|
- opal/clearwater/virtual_dom.rb
|
147
144
|
- opal/clearwater/virtual_dom/js/virtual_dom.js
|
145
|
+
- spec/clearwater/application_spec.rb
|
146
|
+
- spec/clearwater/cached_render_spec.rb
|
147
|
+
- spec/clearwater/router_spec.rb
|
148
|
+
- spec/component_spec.rb
|
148
149
|
homepage: https://clearwater-rb.github.io/
|
149
150
|
licenses:
|
150
151
|
- MIT
|
@@ -169,5 +170,9 @@ rubygems_version: 2.4.8
|
|
169
170
|
signing_key:
|
170
171
|
specification_version: 4
|
171
172
|
summary: Front-end web framework built on Opal
|
172
|
-
test_files:
|
173
|
+
test_files:
|
174
|
+
- spec/clearwater/application_spec.rb
|
175
|
+
- spec/clearwater/cached_render_spec.rb
|
176
|
+
- spec/clearwater/router_spec.rb
|
177
|
+
- spec/component_spec.rb
|
173
178
|
has_rdoc:
|
@@ -1,90 +0,0 @@
|
|
1
|
-
require "clearwater/cgi"
|
2
|
-
require "browser/http"
|
3
|
-
|
4
|
-
module Clearwater
|
5
|
-
class APIClient
|
6
|
-
ResponseNotFinished = Class.new(RuntimeError)
|
7
|
-
|
8
|
-
attr_reader :base_url
|
9
|
-
|
10
|
-
def initialize attributes={}
|
11
|
-
@base_url = attributes.fetch(:base_url) { nil }
|
12
|
-
end
|
13
|
-
|
14
|
-
def fetch resource, id, params
|
15
|
-
response = Browser::HTTP.get(path_for_resource(resource, id, params))
|
16
|
-
Response.new(response)
|
17
|
-
end
|
18
|
-
|
19
|
-
def store resource, id, data={}
|
20
|
-
path = case id
|
21
|
-
when String, Numeric
|
22
|
-
path_for_resource(resource, id)
|
23
|
-
when Hash
|
24
|
-
data.merge! id
|
25
|
-
path_for_resource(resource)
|
26
|
-
end
|
27
|
-
|
28
|
-
response = Browser::HTTP.post(path, data)
|
29
|
-
Response.new(response)
|
30
|
-
end
|
31
|
-
|
32
|
-
def update resource, id, data
|
33
|
-
response = Browser::HTTP.patch path_for_resource(resource, id), data: data
|
34
|
-
Response.new(response)
|
35
|
-
end
|
36
|
-
|
37
|
-
def delete resource, id
|
38
|
-
response = Browser::HTTP.delete path_for_resource(resource, id)
|
39
|
-
Response.new(response)
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def path_for_resource resource, id, params={}
|
45
|
-
path = "#{base_url}/#{resource}"
|
46
|
-
case id
|
47
|
-
when String, Numeric
|
48
|
-
path += "/#{id}"
|
49
|
-
when Hash
|
50
|
-
params.merge! id
|
51
|
-
end
|
52
|
-
|
53
|
-
if params.any?
|
54
|
-
path += "?#{query_string(params)}"
|
55
|
-
end
|
56
|
-
|
57
|
-
path
|
58
|
-
end
|
59
|
-
|
60
|
-
def query_string params
|
61
|
-
query_params = params.map { |key, value|
|
62
|
-
"#{CGI.escape(key)}=#{CGI.escape(value)}"
|
63
|
-
}.join("&")
|
64
|
-
end
|
65
|
-
|
66
|
-
class Response
|
67
|
-
attr_reader :response
|
68
|
-
|
69
|
-
def initialize response
|
70
|
-
@response = response
|
71
|
-
end
|
72
|
-
|
73
|
-
def then
|
74
|
-
response.then do |r|
|
75
|
-
yield self.class.new(r)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def fail
|
80
|
-
response.fail do |r|
|
81
|
-
yield self.class.new(r)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def json
|
86
|
-
response.json
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
data/opal/clearwater/cgi.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
module Clearwater
|
2
|
-
class CGI
|
3
|
-
def self.escape string
|
4
|
-
# string.gsub(/([^ a-zA-Z0-9_.-]+)/) do |ch|
|
5
|
-
string.chars.map do |ch|
|
6
|
-
if ch =~ /([^ a-zA-Z0-9_.-]+)/
|
7
|
-
"%#{ch.ord.to_s(16).upcase}"
|
8
|
-
else
|
9
|
-
ch
|
10
|
-
end
|
11
|
-
end.join.tr(" ", "+")
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
data/opal/clearwater/store.rb
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
require "json"
|
2
|
-
|
3
|
-
module Clearwater
|
4
|
-
class Store
|
5
|
-
attr_reader :mapping, :identity_map
|
6
|
-
|
7
|
-
def initialize options={}
|
8
|
-
@url = options.fetch(:url) { "/api/:model/:id" }
|
9
|
-
@mapping = Mapping.new(options.fetch(:mapping) { {} })
|
10
|
-
@protocol = options.fetch(:protocol) { HTTP }
|
11
|
-
@identity_map = IdentityMap.new
|
12
|
-
end
|
13
|
-
|
14
|
-
def all klass
|
15
|
-
deserialized = api(:get, url_for(klass)).body
|
16
|
-
models = JSON.parse(deserialized).map do |attributes|
|
17
|
-
deserialize(klass, attributes)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def find klass, id
|
22
|
-
identity_map.fetch(klass, id) do
|
23
|
-
serialized = api(:get, url_for(klass, id)).body
|
24
|
-
identity_map[klass][id] = deserialize(klass, JSON.parse(serialized))
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def save model
|
29
|
-
method = persisted?(model) ? :patch : :post
|
30
|
-
response = api method, url_for_model(model)
|
31
|
-
if method == :post && response.ok
|
32
|
-
attributes = JSON.parse(response.body)
|
33
|
-
model.instance_variable_set :@id, attributes[:id]
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def delete model
|
38
|
-
api :delete, url_for_model(model)
|
39
|
-
end
|
40
|
-
|
41
|
-
def persisted? model
|
42
|
-
!!model.id
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def api method, url
|
48
|
-
@protocol.public_send method, url
|
49
|
-
end
|
50
|
-
|
51
|
-
def url_for klass, id=nil
|
52
|
-
@url.gsub(":model", mapping[klass])
|
53
|
-
.gsub(":id", id.to_s)
|
54
|
-
end
|
55
|
-
|
56
|
-
def url_for_model model
|
57
|
-
url_for(model.class, model.id)
|
58
|
-
end
|
59
|
-
|
60
|
-
def url_for_class klass, id
|
61
|
-
url_for(klass, nil)
|
62
|
-
end
|
63
|
-
|
64
|
-
def deserialize klass, attributes
|
65
|
-
model = klass.allocate
|
66
|
-
attributes.each do |attr, value|
|
67
|
-
model.instance_variable_set "@#{attr}", value
|
68
|
-
end
|
69
|
-
|
70
|
-
model
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
class Mapping
|
75
|
-
def initialize mappings={}
|
76
|
-
@custom_mappings = mappings
|
77
|
-
end
|
78
|
-
|
79
|
-
def [] klass
|
80
|
-
@custom_mappings.fetch(klass) { |*args|
|
81
|
-
klass.name.downcase.gsub("::", "/") + "s"
|
82
|
-
}
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
class IdentityMap
|
87
|
-
def initialize
|
88
|
-
@map = Hash.new { |h, k| h[k] = {} }
|
89
|
-
end
|
90
|
-
|
91
|
-
def [] key
|
92
|
-
@map[key]
|
93
|
-
end
|
94
|
-
|
95
|
-
def fetch(klass, id, &block)
|
96
|
-
self[klass].fetch(id, &block)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|