isomorfeus-preact 10.5.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 +7 -0
- data/README.md +62 -0
- data/lib/browser/delegate_native.rb +70 -0
- data/lib/browser/element.rb +176 -0
- data/lib/browser/element/canvas.rb +17 -0
- data/lib/browser/element/media.rb +78 -0
- data/lib/browser/event.rb +92 -0
- data/lib/browser/event_target.rb +39 -0
- data/lib/browser/file_list.rb +125 -0
- data/lib/browser/iterable.rb +15 -0
- data/lib/isomorfeus-preact.rb +109 -0
- data/lib/isomorfeus/preact/config.rb +189 -0
- data/lib/isomorfeus/preact/memcached_component_cache.rb +19 -0
- data/lib/isomorfeus/preact/redis_component_cache.rb +19 -0
- data/lib/isomorfeus/preact/thread_local_component_cache.rb +17 -0
- data/lib/isomorfeus/preact_view_helper.rb +188 -0
- data/lib/isomorfeus/props/validate_hash_proxy.rb +186 -0
- data/lib/isomorfeus/props/validator.rb +159 -0
- data/lib/isomorfeus/top_level.rb +101 -0
- data/lib/isomorfeus/top_level_ssr.rb +27 -0
- data/lib/isomorfeus_preact/lucid_app/api.rb +22 -0
- data/lib/isomorfeus_preact/lucid_app/base.rb +7 -0
- data/lib/isomorfeus_preact/lucid_app/mixin.rb +16 -0
- data/lib/isomorfeus_preact/lucid_app/native_component_constructor.rb +91 -0
- data/lib/isomorfeus_preact/lucid_component/api.rb +68 -0
- data/lib/isomorfeus_preact/lucid_component/app_store_proxy.rb +37 -0
- data/lib/isomorfeus_preact/lucid_component/base.rb +7 -0
- data/lib/isomorfeus_preact/lucid_component/class_store_proxy.rb +44 -0
- data/lib/isomorfeus_preact/lucid_component/initializer.rb +14 -0
- data/lib/isomorfeus_preact/lucid_component/instance_store_proxy.rb +44 -0
- data/lib/isomorfeus_preact/lucid_component/mixin.rb +15 -0
- data/lib/isomorfeus_preact/lucid_component/native_component_constructor.rb +84 -0
- data/lib/isomorfeus_preact/lucid_component/styles_api.rb +31 -0
- data/lib/isomorfeus_preact/lucid_component/styles_wrapper.rb +40 -0
- data/lib/isomorfeus_preact/lucid_func/base.rb +7 -0
- data/lib/isomorfeus_preact/lucid_func/initializer.rb +11 -0
- data/lib/isomorfeus_preact/lucid_func/mixin.rb +12 -0
- data/lib/isomorfeus_preact/lucid_func/native_component_constructor.rb +55 -0
- data/lib/isomorfeus_preact/preact/function_component/api.rb +123 -0
- data/lib/isomorfeus_preact/preact/function_component/base.rb +9 -0
- data/lib/isomorfeus_preact/preact/function_component/initializer.rb +10 -0
- data/lib/isomorfeus_preact/preact/function_component/mixin.rb +12 -0
- data/lib/isomorfeus_preact/preact/function_component/native_component_constructor.rb +48 -0
- data/lib/lucid_app/context.rb +24 -0
- data/lib/lucid_prop_declaration/mixin.rb +126 -0
- data/lib/preact.rb +309 -0
- data/lib/preact/component/api.rb +124 -0
- data/lib/preact/component/base.rb +9 -0
- data/lib/preact/component/callbacks.rb +102 -0
- data/lib/preact/component/elements.rb +64 -0
- data/lib/preact/component/initializer.rb +11 -0
- data/lib/preact/component/mixin.rb +15 -0
- data/lib/preact/component/native_component_constructor.rb +65 -0
- data/lib/preact/component/params.rb +18 -0
- data/lib/preact/component/props.rb +55 -0
- data/lib/preact/component/resolution.rb +97 -0
- data/lib/preact/component/state.rb +58 -0
- data/lib/preact/context_wrapper.rb +46 -0
- data/lib/preact/native_constant_wrapper.rb +29 -0
- data/lib/preact/options.rb +98 -0
- data/lib/preact/ref.rb +17 -0
- data/lib/preact/version.rb +3 -0
- metadata +301 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module Preact
|
3
|
+
class MemcachedComponentCache
|
4
|
+
def initialize(*args)
|
5
|
+
@dalli_client = Dalli::Client.new(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch(key)
|
9
|
+
json = @dalli_client.get(key)
|
10
|
+
Oj.load(json, mode: :strict)
|
11
|
+
end
|
12
|
+
|
13
|
+
def store(key, rendered_tree, response_status, styles)
|
14
|
+
json = Oj.dump([rendered_tree, response_status, styles], mode: :strict)
|
15
|
+
@dalli_client.set(key, json)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module Preact
|
3
|
+
class RedisComponentCache
|
4
|
+
def initialize(*args)
|
5
|
+
@redis_client = Redis.new(@args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch(key)
|
9
|
+
json = @redis_client.get(key)
|
10
|
+
Oj.load(json, mode: :strict)
|
11
|
+
end
|
12
|
+
|
13
|
+
def store(key, rendered_tree, response_status, styles)
|
14
|
+
json = Oj.dump([rendered_tree, response_status, styles], mode: :strict)
|
15
|
+
@redis_client.set(key, json)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module Preact
|
3
|
+
class ThreadLocalComponentCache
|
4
|
+
def initialize
|
5
|
+
Thread.current[:local_cache] = {} unless Thread.current.key?(:local_cache)
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch(key)
|
9
|
+
Thread.current[:local_cache][key]
|
10
|
+
end
|
11
|
+
|
12
|
+
def store(key, rendered_tree, response_status, styles)
|
13
|
+
Thread.current[:local_cache][key] = [rendered_tree, response_status, styles]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module PreactViewHelper
|
3
|
+
def cached_mount_component(component_name, props = {}, asset = 'web_ssr.js')
|
4
|
+
key = "#{component_name}#{props}#{asset}"
|
5
|
+
if Isomorfeus.production?
|
6
|
+
render_result, @ssr_response_status, @ssr_styles = component_cache.fetch(key)
|
7
|
+
return render_result if render_result
|
8
|
+
end
|
9
|
+
render_result = mount_component(component_name, props, asset)
|
10
|
+
status = ssr_response_status
|
11
|
+
component_cache.store(key, render_result, status, ssr_styles) if status >= 200 && status < 300
|
12
|
+
render_result
|
13
|
+
end
|
14
|
+
|
15
|
+
def mount_component(component_name, props = {}, asset = 'web_ssr.js')
|
16
|
+
@ssr_response_status = nil
|
17
|
+
@ssr_styles = nil
|
18
|
+
thread_id_asset = "#{Thread.current.object_id}#{asset}"
|
19
|
+
render_result = "<div data-iso-env=\"#{Isomorfeus.env}\" data-iso-root=\"#{component_name}\" data-iso-props='#{Oj.dump(props, mode: :strict)}'"
|
20
|
+
if Isomorfeus.server_side_rendering
|
21
|
+
|
22
|
+
if Isomorfeus.development?
|
23
|
+
# always create a new context, effectively reloading code
|
24
|
+
# delete the existing context first, saves memory
|
25
|
+
if Isomorfeus.ssr_contexts.key?(thread_id_asset)
|
26
|
+
uuid = Isomorfeus.ssr_contexts[thread_id_asset].instance_variable_get(:@uuid)
|
27
|
+
runtime = Isomorfeus.ssr_contexts[thread_id_asset].instance_variable_get(:@runtime)
|
28
|
+
runtime.vm.delete_context(uuid)
|
29
|
+
end
|
30
|
+
asset_path = "#{Isomorfeus.ssr_hot_asset_url}#{asset}"
|
31
|
+
begin
|
32
|
+
asset = Net::HTTP.get(URI(asset_path))
|
33
|
+
rescue Exception => e
|
34
|
+
# STDERR.puts "Server Side Rendering: Failed loading asset #{asset_path} from webpack dev server. Error: #{e.message}"
|
35
|
+
# STDERR.puts e.backtrace.join("\n")
|
36
|
+
Isomorfeus.raise_error(message: "Server Side Rendering: Failed loading asset #{asset_path} from webpack dev server. Error: #{e.message}", stack: e.backtrace )
|
37
|
+
end
|
38
|
+
if asset.strip.start_with?('<')
|
39
|
+
# STDERR.puts "Server Side Rendering: Failed loading asset #{asset_path} from webpack dev server, asset is not javascript. Did the webpack build succeed?"
|
40
|
+
Isomorfeus.raise_error(message: "Server Side Rendering: Failed loading asset #{asset_path} from webpack dev server, asset is not javascript. Did the webpack build succeed?")
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
Isomorfeus.ssr_contexts[thread_id_asset] = ExecJS.permissive_compile(asset)
|
44
|
+
rescue Exception => e
|
45
|
+
# STDERR.puts "Server Side Rendering: Failed creating context for #{asset_path}. Error: #{e.message}"
|
46
|
+
# STDERR.puts e.backtrace.join("\n")
|
47
|
+
Isomorfeus.raise_error(message: "Server Side Rendering: Failed creating context for #{asset_path}. Error: #{e.message}", stack: e.backtrace)
|
48
|
+
end
|
49
|
+
else
|
50
|
+
# initialize speednode context
|
51
|
+
unless Isomorfeus.ssr_contexts.key?(thread_id_asset)
|
52
|
+
asset_file_name = OpalWebpackLoader::Manifest.lookup_path_for(asset)
|
53
|
+
Isomorfeus.raise_error(message: "Server Side Rendering: Build asset file not found for #{asset}. Has it been build?") unless asset_file_name
|
54
|
+
asset_path = File.join('public', asset_file_name)
|
55
|
+
Isomorfeus.ssr_contexts[thread_id_asset] = ExecJS.permissive_compile(File.read(asset_path))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# build javascript for rendering first pass
|
60
|
+
# it will initialize buffers to guard against leaks, maybe caused by previous exceptions
|
61
|
+
javascript = <<~JAVASCRIPT
|
62
|
+
global.Opal.Preact.render_buffer = [];
|
63
|
+
global.Opal.Preact.active_components = [];
|
64
|
+
global.Opal.Preact.active_redux_components = [];
|
65
|
+
global.FirstPassFinished = false;
|
66
|
+
global.Exception = false;
|
67
|
+
global.IsomorfeusSessionId = '#{Thread.current[:isomorfeus_session_id]}';
|
68
|
+
global.Opal.Isomorfeus['$env=']('#{Isomorfeus.env}');
|
69
|
+
if (typeof global.Opal.Isomorfeus.$negotiated_locale === 'function') {
|
70
|
+
global.Opal.Isomorfeus["$negotiated_locale="]('#{props[:locale]}');
|
71
|
+
}
|
72
|
+
global.Opal.Isomorfeus['$force_init!']();
|
73
|
+
global.Opal.Isomorfeus['$ssr_response_status='](200);
|
74
|
+
global.Opal.Isomorfeus.TopLevel['$ssr_route_path=']('#{props[:location]}');
|
75
|
+
JAVASCRIPT
|
76
|
+
|
77
|
+
# if location_host and scheme are given and if Transport is loaded, connect and then render,
|
78
|
+
# otherwise do not render because only one pass is required
|
79
|
+
ws_scheme = props[:location_scheme] == 'https:' ? 'wss:' : 'ws:'
|
80
|
+
location_host = props[:location_host] ? props[:location_host] : 'localhost'
|
81
|
+
api_ws_path = Isomorfeus.respond_to?(:api_websocket_path) ? Isomorfeus.api_websocket_path : ''
|
82
|
+
transport_ws_url = ws_scheme + location_host + api_ws_path
|
83
|
+
javascript << <<~JAVASCRIPT
|
84
|
+
let api_ws_path = '#{api_ws_path}';
|
85
|
+
let exception;
|
86
|
+
if (typeof global.Opal.Isomorfeus.Transport !== 'undefined' && api_ws_path !== '') {
|
87
|
+
global.Opal.Isomorfeus.TopLevel["$transport_ws_url="]("#{transport_ws_url}");
|
88
|
+
global.Opal.send(global.Opal.Isomorfeus.Transport.$promise_connect(), 'then', [], ($$1 = function(){
|
89
|
+
try {
|
90
|
+
global.Opal.Isomorfeus.TopLevel.$render_component_to_string('#{component_name}', #{Oj.dump(props, mode: :strict)});
|
91
|
+
global.FirstPassFinished = 'transport';
|
92
|
+
} catch (e) {
|
93
|
+
global.Exception = e;
|
94
|
+
global.FirstPassFinished = 'transport';
|
95
|
+
}
|
96
|
+
}, $$1.$$s = this, $$1.$$arity = 0, $$1))
|
97
|
+
} else { return global.FirstPassFinished = true; };
|
98
|
+
JAVASCRIPT
|
99
|
+
|
100
|
+
# execute first render pass
|
101
|
+
first_pass_skipped = Isomorfeus.ssr_contexts[thread_id_asset].exec(javascript)
|
102
|
+
|
103
|
+
# wait for first pass to finish
|
104
|
+
unless first_pass_skipped
|
105
|
+
first_pass_finished, exception = Isomorfeus.ssr_contexts[thread_id_asset].exec('return [global.FirstPassFinished, global.Exception ? { message: global.Exception.message, stack: global.Exception.stack } : false ]')
|
106
|
+
Isomorfeus.raise_error(message: "Server Side Rendering: #{exception['message']}", stack: exception['stack']) if exception
|
107
|
+
unless first_pass_finished
|
108
|
+
start_time = Time.now
|
109
|
+
while !first_pass_finished
|
110
|
+
break if (Time.now - start_time) > 10
|
111
|
+
sleep 0.01
|
112
|
+
first_pass_finished = Isomorfeus.ssr_contexts[thread_id_asset].exec('return global.FirstPassFinished')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# wait for transport requests to finish
|
117
|
+
if first_pass_finished == 'transport'
|
118
|
+
transport_busy = Isomorfeus.ssr_contexts[thread_id_asset].exec('return global.Opal.Isomorfeus.Transport["$busy?"]()')
|
119
|
+
if transport_busy
|
120
|
+
start_time = Time.now
|
121
|
+
while transport_busy
|
122
|
+
break if (Time.now - start_time) > 10
|
123
|
+
sleep 0.01
|
124
|
+
transport_busy = Isomorfeus.ssr_contexts[thread_id_asset].exec('return global.Opal.Isomorfeus.Transport["$busy?"]()')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# build javascript for second render pass
|
131
|
+
# guard against leaks from first pass, maybe because of a exception
|
132
|
+
javascript = <<~JAVASCRIPT
|
133
|
+
global.Opal.Preact.render_buffer = [];
|
134
|
+
global.Opal.Preact.active_components = [];
|
135
|
+
global.Opal.Preact.active_redux_components = [];
|
136
|
+
global.Exception = false;
|
137
|
+
let rendered_tree;
|
138
|
+
let ssr_styles;
|
139
|
+
let component;
|
140
|
+
try {
|
141
|
+
rendered_tree = global.Opal.Isomorfeus.TopLevel.$render_component_to_string('#{component_name}', #{Oj.dump(props, mode: :strict)});
|
142
|
+
} catch (e) {
|
143
|
+
global.Exception = e;
|
144
|
+
}
|
145
|
+
let application_state = global.Opal.Isomorfeus.store.native.getState();
|
146
|
+
if (typeof global.Opal.Isomorfeus.Transport !== 'undefined') { global.Opal.Isomorfeus.Transport.$disconnect(); }
|
147
|
+
if (typeof global.NanoCSSInstance !== 'undefined') { ssr_styles = global.NanoCSSInstance.raw }
|
148
|
+
return [rendered_tree, application_state, ssr_styles, global.Opal.Isomorfeus['$ssr_response_status'](), global.Exception ? { message: global.Exception.message, stack: global.Exception.stack } : false];
|
149
|
+
JAVASCRIPT
|
150
|
+
|
151
|
+
# execute second render pass
|
152
|
+
rendered_tree, application_state, @ssr_styles, @ssr_response_status, exception = Isomorfeus.ssr_contexts[thread_id_asset].exec(javascript)
|
153
|
+
Isomorfeus.raise_error(message: exception['message'], stack: exception['stack']) if exception
|
154
|
+
|
155
|
+
render_result << " data-iso-hydrated='true'" if rendered_tree
|
156
|
+
if Isomorfeus.respond_to?(:current_user) && Isomorfeus.current_user && !Isomorfeus.current_user.anonymous?
|
157
|
+
render_result << " data-iso-usid=#{Oj.dump(Isomorfeus.current_user.to_sid, mode: :strict)}"
|
158
|
+
end
|
159
|
+
render_result << " data-iso-nloc='#{props[:locale]}'>"
|
160
|
+
render_result << (rendered_tree ? rendered_tree : "SSR didn't work")
|
161
|
+
else
|
162
|
+
if Isomorfeus.respond_to?(:current_user) && Isomorfeus.current_user && !Isomorfeus.current_user.anonymous?
|
163
|
+
render_result << " data-iso-usid=#{Oj.dump(Isomorfeus.current_user.to_sid, mode: :strict)}"
|
164
|
+
end
|
165
|
+
render_result << " data-iso-nloc='#{props[:locale]}'>"
|
166
|
+
end
|
167
|
+
render_result << '</div>'
|
168
|
+
if Isomorfeus.server_side_rendering
|
169
|
+
render_result = "<script type='application/javascript'>\nServerSideRenderingStateJSON = #{Oj.dump(application_state, mode: :strict)}\n</script>\n" << render_result
|
170
|
+
end
|
171
|
+
render_result
|
172
|
+
end
|
173
|
+
|
174
|
+
def ssr_response_status
|
175
|
+
@ssr_response_status || 200
|
176
|
+
end
|
177
|
+
|
178
|
+
def ssr_styles
|
179
|
+
@ssr_styles || ''
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def component_cache
|
185
|
+
@_component_cache ||= Isomorfeus.component_cache_init_block.call
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module Props
|
3
|
+
class ValidateHashProxy
|
4
|
+
def initialize
|
5
|
+
@validation_hash = { required: true, validate: {} }
|
6
|
+
end
|
7
|
+
|
8
|
+
def is
|
9
|
+
self
|
10
|
+
end
|
11
|
+
alias_method :and, :is
|
12
|
+
alias_method :has, :is
|
13
|
+
alias_method :with, :is
|
14
|
+
|
15
|
+
def allow_nil
|
16
|
+
@validation_hash[:allow_nil] = true
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def cast
|
21
|
+
@validation_hash[:cast] = true
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def default(v)
|
26
|
+
@validation_hash[:required] = false
|
27
|
+
@validation_hash[:default] = v
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def ensure(v = nil, &block)
|
32
|
+
if block_given?
|
33
|
+
@validation_hash[:ensure_block] = block
|
34
|
+
else
|
35
|
+
@validation_hash[:ensure] = v
|
36
|
+
end
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def exact_class(t_class)
|
41
|
+
@validation_hash[:class] = t_class
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def greater_than(v)
|
46
|
+
@validation_hash[:validate][:gt] = v
|
47
|
+
self
|
48
|
+
end
|
49
|
+
alias_method :gt, :greater_than
|
50
|
+
|
51
|
+
def is_a(i_class)
|
52
|
+
@validation_hash[:is_a] = i_class
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def keys(*keys)
|
57
|
+
@validation_hash[:validate][:hash_keys] = keys
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def size(l)
|
62
|
+
@validation_hash[:validate][:size] = v
|
63
|
+
self
|
64
|
+
end
|
65
|
+
alias_method :length, :size
|
66
|
+
|
67
|
+
def less_than(v)
|
68
|
+
@validation_hash[:validate][:lt] = v
|
69
|
+
self
|
70
|
+
end
|
71
|
+
alias_method :lt, :less_than
|
72
|
+
|
73
|
+
def matches(regexp)
|
74
|
+
@validation_hash[:validate][:matches] = regexp
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
def max(l)
|
79
|
+
@validation_hash[:validate][:max] = l
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def max_size(l)
|
84
|
+
@validation_hash[:validate][:max_size] = l
|
85
|
+
self
|
86
|
+
end
|
87
|
+
alias_method :max_length, :max_size
|
88
|
+
|
89
|
+
def min(l)
|
90
|
+
@validation_hash[:validate][:min] = l
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def min_size(l)
|
95
|
+
@validation_hash[:validate][:min_size] = l
|
96
|
+
self
|
97
|
+
end
|
98
|
+
alias_method :min_length, :min_size
|
99
|
+
|
100
|
+
def negative
|
101
|
+
@validation_hash[:validate][:direction] = :negative
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
def optional
|
106
|
+
@validation_hash[:required] = false
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
def positive
|
111
|
+
@validation_hash[:validate][:direction] = :positive
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
def required
|
116
|
+
@validation_hash[:required] = true
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def test(&block)
|
121
|
+
@validation_hash[:validate][:test] = block
|
122
|
+
self
|
123
|
+
end
|
124
|
+
alias_method :condition, :test
|
125
|
+
alias_method :check, :test
|
126
|
+
|
127
|
+
def validate_block(&block)
|
128
|
+
@validation_hash[:validate][:validate_block] = block
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
# types
|
133
|
+
|
134
|
+
def Array
|
135
|
+
@validation_hash[:class] = Array
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
def Boolean
|
140
|
+
@validation_hash[:type] = :boolean
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
def Enumerable
|
145
|
+
@validation_hash[:is_a] = Enumerable
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
def Float
|
150
|
+
@validation_hash[:class] = Float
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
def Hash
|
155
|
+
@validation_hash[:class] = Hash
|
156
|
+
self
|
157
|
+
end
|
158
|
+
|
159
|
+
def Integer
|
160
|
+
@validation_hash[:class] = Integer
|
161
|
+
self
|
162
|
+
end
|
163
|
+
|
164
|
+
def String
|
165
|
+
@validation_hash[:class] = String
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
# sub types
|
170
|
+
|
171
|
+
def Email
|
172
|
+
@validation_hash[:type] = :email
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
def Url
|
177
|
+
@validation_hash[:type] = :uri
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
def to_h
|
182
|
+
@validation_hash
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module Props
|
3
|
+
class Validator
|
4
|
+
def initialize(source_class, prop, value, options)
|
5
|
+
@c = source_class
|
6
|
+
@p = prop
|
7
|
+
@v = value
|
8
|
+
@o = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate!
|
12
|
+
ensured = ensure!
|
13
|
+
unless ensured
|
14
|
+
set_default_value
|
15
|
+
cast!
|
16
|
+
type!
|
17
|
+
end
|
18
|
+
run_checks!
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def validated_value
|
23
|
+
validate!
|
24
|
+
@v
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# basic tests
|
30
|
+
|
31
|
+
def set_default_value
|
32
|
+
return unless @v.nil?
|
33
|
+
@v = @o[:default] if @o.key?(:default)
|
34
|
+
end
|
35
|
+
|
36
|
+
def cast!
|
37
|
+
if @o.key?(:cast)
|
38
|
+
begin
|
39
|
+
@v = case @o[:class]
|
40
|
+
when Integer then @v.to_i
|
41
|
+
when String then @v.to_s
|
42
|
+
when Float then @v.to_f
|
43
|
+
when Array then @v.to_a
|
44
|
+
when Hash then @v.to_h
|
45
|
+
end
|
46
|
+
@v = !!@v if @o[:type] == :boolean
|
47
|
+
rescue
|
48
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} cast failed") unless @v.class == @o[:class]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def ensure!
|
54
|
+
if @o.key?(:ensure)
|
55
|
+
@v = @o[:ensure] unless @v
|
56
|
+
true
|
57
|
+
elsif @o.key?(:ensure_block)
|
58
|
+
@v = @o[:ensure_block].call(@v)
|
59
|
+
true
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def type!
|
66
|
+
return if @o[:allow_nil] && @v.nil?
|
67
|
+
if @o.key?(:class)
|
68
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} class not #{@o[:class]}") unless @v.class == @o[:class]
|
69
|
+
elsif @o.key?(:is_a)
|
70
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is not a #{@o[:is_a]}") unless @v.is_a?(@o[:is_a])
|
71
|
+
elsif @o.key?(:type)
|
72
|
+
case @o[:type]
|
73
|
+
when :boolean
|
74
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is not a boolean") unless @v.class == TrueClass || @v.class == FalseClass
|
75
|
+
else
|
76
|
+
c_string_sub_types
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# all other checks
|
82
|
+
|
83
|
+
def run_checks!
|
84
|
+
if @o.key?(:validate)
|
85
|
+
@o[:validate].each do |m, l|
|
86
|
+
send('c_' + m, l)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
@o[:validate_block].call(@v) if @o.key?(:validate_block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# specific validations
|
93
|
+
def c_gt(v)
|
94
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} not greater than #{v}!") unless @v > v
|
95
|
+
end
|
96
|
+
|
97
|
+
def c_lt(v)
|
98
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} not less than #{v}!") unless @v < v
|
99
|
+
end
|
100
|
+
|
101
|
+
def c_keys(v)
|
102
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} keys dont fit!") unless @v.keys.sort == v.sort
|
103
|
+
end
|
104
|
+
|
105
|
+
def c_size(v)
|
106
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} length/size is not #{v}") unless @v.size == v
|
107
|
+
end
|
108
|
+
|
109
|
+
def c_matches(v)
|
110
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} does not match #{v}") unless v.match?(@v)
|
111
|
+
end
|
112
|
+
|
113
|
+
def c_max(v)
|
114
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is larger than #{v}") unless @v <= v
|
115
|
+
end
|
116
|
+
|
117
|
+
def c_min(v)
|
118
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is smaller than #{v}") unless @v >= v
|
119
|
+
end
|
120
|
+
|
121
|
+
def c_max_size(v)
|
122
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is larger than #{v}") unless @v.size <= v
|
123
|
+
end
|
124
|
+
|
125
|
+
def c_min_size(v)
|
126
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is smaller than #{v}") unless @v.size >= v
|
127
|
+
end
|
128
|
+
|
129
|
+
def c_direction(v)
|
130
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is positive") if v == :negative && @v >= 0
|
131
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is negative") if v == :positive && @v < 0
|
132
|
+
end
|
133
|
+
|
134
|
+
def c_test
|
135
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} test condition check failed") unless @o[:test].call(@v)
|
136
|
+
end
|
137
|
+
|
138
|
+
def c_string_sub_types
|
139
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} must be a String") unless @v.class == String
|
140
|
+
case @o[:type]
|
141
|
+
when :email
|
142
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is not a valid email address") unless @v.match?(/\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/)
|
143
|
+
when :uri
|
144
|
+
if RUBY_ENGINE == 'opal'
|
145
|
+
%x{
|
146
|
+
try {
|
147
|
+
new URL(#@v);
|
148
|
+
} catch {
|
149
|
+
#{Isomorfeus.raise_error(message: "#{@c}: #{@p} is not a valid uri")}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
else
|
153
|
+
Isomorfeus.raise_error(message: "#{@c}: #{@p} is not a valid uri") unless @v.match?(/\A#{URI.regexp}\z/)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|