el 0.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 +15 -0
- data/.travis.yml +6 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.md +77 -0
- data/Rakefile +47 -0
- data/docs/Assets.md +157 -0
- data/docs/CRUD.md +255 -0
- data/docs/CacheManager.md +66 -0
- data/docs/ContentHelpers.md +64 -0
- data/docs/TagFactory.md +201 -0
- data/el.gemspec +23 -0
- data/lib/el.rb +11 -0
- data/lib/el/assets.rb +126 -0
- data/lib/el/cache.rb +96 -0
- data/lib/el/constants.rb +21 -0
- data/lib/el/content_helpers.rb +61 -0
- data/lib/el/crud.rb +239 -0
- data/lib/el/ipcm.rb +67 -0
- data/lib/el/tag_factory.rb +180 -0
- data/lib/el/utils.rb +37 -0
- data/test/assets/master.css +1 -0
- data/test/assets/master.js +1 -0
- data/test/assets/master.png +0 -0
- data/test/helpers.rb +14 -0
- data/test/ipcm/config.ru +25 -0
- data/test/ipcm/view/compiler_test.erb +1 -0
- data/test/setup.rb +11 -0
- data/test/sprockets/app.css +5 -0
- data/test/sprockets/app.js +7 -0
- data/test/sprockets/ui.css +3 -0
- data/test/sprockets/ui.js +5 -0
- data/test/test__assets.rb +276 -0
- data/test/test__cache.rb +145 -0
- data/test/test__content_helpers.rb +41 -0
- data/test/test__crud.rb +269 -0
- data/test/test__ipcm.rb +127 -0
- data/test/test__sprockets.rb +101 -0
- data/test/test__tag_factory.rb +170 -0
- metadata +124 -0
data/lib/el/cache.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
class E
|
2
|
+
def cache(*a, &b); app.cache(*a, &b); end
|
3
|
+
def clear_cache!(*a); app.clear_cache!(*a); end
|
4
|
+
def cache_pool; app.cache_pool; end
|
5
|
+
end
|
6
|
+
|
7
|
+
class EBuilder
|
8
|
+
|
9
|
+
# very basic cache implementation.
|
10
|
+
# by default the cache will be kept in memory.
|
11
|
+
# if you want to use a different pool,
|
12
|
+
# set it by using `cache_pool` at app level.
|
13
|
+
# make sure your pool behaves just like a Hash,
|
14
|
+
# meant it responds to `[]=`, `[]`, `keys`, `delete` and `clear`
|
15
|
+
#
|
16
|
+
def cache_pool pool = nil
|
17
|
+
return @cache_pool if @cache_pool
|
18
|
+
@cache_pool = pool if pool
|
19
|
+
@cache_pool ||= Hash.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# simply running a block and store returned value.
|
23
|
+
# on next request the stored value will be returned.
|
24
|
+
#
|
25
|
+
# @note
|
26
|
+
# value is not stored if block returns false or nil
|
27
|
+
#
|
28
|
+
def cache key = nil, &proc
|
29
|
+
key ||= proc.source_location
|
30
|
+
cache_pool[key] || ( (val = proc.call) && (cache_pool[key] = val) )
|
31
|
+
end
|
32
|
+
|
33
|
+
# a simple way to manage stored cache.
|
34
|
+
# any number of arguments(actually matchers) accepted.
|
35
|
+
# matchers can be of String, Symbol or Regexp type. any other arguments ignored
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# class App < E
|
39
|
+
#
|
40
|
+
# before do
|
41
|
+
# if 'some condition occurred'
|
42
|
+
# # clearing cache only for @red_banners and @db_items
|
43
|
+
# clear_cache! :red_banners, :db_items
|
44
|
+
# end
|
45
|
+
# if 'some another condition occurred'
|
46
|
+
# # clearing all cache
|
47
|
+
# clear_cache!
|
48
|
+
# end
|
49
|
+
# if 'Yet another condition occurred'
|
50
|
+
# # clearing cache by regexp
|
51
|
+
# clear_cache! /banners/, /db/
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# def index
|
57
|
+
# @db_items = cache :db_items do
|
58
|
+
# # ...
|
59
|
+
# end
|
60
|
+
# @red_banners = cache :red_banners do
|
61
|
+
# # ...
|
62
|
+
# end
|
63
|
+
# @blue_banners = cache :blue_banners do
|
64
|
+
# # ...
|
65
|
+
# end
|
66
|
+
# # ...
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# def products
|
70
|
+
# cache do
|
71
|
+
# # fetch and render products
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# @param [Array] keys
|
77
|
+
def clear_cache! *matchers
|
78
|
+
clear_cache *matchers
|
79
|
+
ipcm_trigger :clear_cache, *matchers
|
80
|
+
end
|
81
|
+
|
82
|
+
# same as `clear_cache!` except it is working only on current process
|
83
|
+
#
|
84
|
+
# @param [Array] keys
|
85
|
+
def clear_cache *matchers
|
86
|
+
return cache_pool.clear if matchers.empty?
|
87
|
+
keys = Array.new(cache_pool.keys)
|
88
|
+
matchers.each do |matcher|
|
89
|
+
mkeys = matcher.is_a?(Regexp) ?
|
90
|
+
keys.select {|k| k =~ matcher} :
|
91
|
+
keys.select {|k| k == matcher}
|
92
|
+
mkeys.each {|k| cache_pool.delete k}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/lib/el/constants.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module EConstants
|
2
|
+
GENERIC_TAGS = %w[
|
3
|
+
html head title body h1 h2 h3 h4 h5 h6 p a
|
4
|
+
abbr address b bdi bdo blockquote cite code del dfn em i ins kbd
|
5
|
+
mark meter pre progress q rp rt ruby s samp small strong sub sup
|
6
|
+
time u var wbr form textarea button select optgroup option label
|
7
|
+
fieldset legend datalist output iframe map area canvas figcaption figure
|
8
|
+
audio source track video nav ul ol li dl dt dd
|
9
|
+
table caption th tr td thead tbody tfoot colgroup col
|
10
|
+
div span header footer hgroup section article aside details dialog summary
|
11
|
+
style script noscript object
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
EMPTY_TAGS = %w[
|
15
|
+
img link br hr input keygen meta base embed param
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
IMAGE_TAGS = %w[
|
19
|
+
jpg jpeg png gif tif tiff bmp svg xpm
|
20
|
+
].freeze
|
21
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module EL
|
2
|
+
module ContentHelpers
|
3
|
+
|
4
|
+
# capture content and then render it into a different place
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# content_for :assets do
|
8
|
+
# js_tag :jquery
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# p content_for?(:assets)
|
12
|
+
# #=> #<Proc:0x00...
|
13
|
+
#
|
14
|
+
# yield_content :assets
|
15
|
+
# #=> '<script src="jquery.js" type="text/javascript"></script>'
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# content_for :account do |name, email|
|
19
|
+
# form_tag! do
|
20
|
+
# input_tag(value: name) +
|
21
|
+
# input_tag(value: email)
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# yield_content :account, :foo, 'foo@bar.com'
|
26
|
+
# #=> '<form><input value="foo"><input value="foo@bar.com"></form>'
|
27
|
+
#
|
28
|
+
def content_for key, &proc
|
29
|
+
(@__el__content_for ||= {})[key] = proc
|
30
|
+
end
|
31
|
+
|
32
|
+
# check whether content block exists for some key
|
33
|
+
def content_for? key
|
34
|
+
(@__el__content_for || {})[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
# render a content block captured by `content_for`
|
38
|
+
def yield_content key, *args
|
39
|
+
(proc = content_for?(key)) && proc.call(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
# execute given content block and return the result.
|
43
|
+
# useful when you need to display same snippet multiple times
|
44
|
+
# and want it rendered only once
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# assets = capture_html do
|
48
|
+
# js_tag(:jquery) +
|
49
|
+
# css_tag(:ui)
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
def capture_html &proc
|
53
|
+
proc.call
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
class E
|
60
|
+
include EL::ContentHelpers
|
61
|
+
end
|
data/lib/el/crud.rb
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
class << E
|
2
|
+
|
3
|
+
E__CRUD__AUTH_TEST_PAYLOAD_KEY = "E__CRUD__AUTH_TEST_PAYLOAD_#{rand(2**64)}".freeze
|
4
|
+
|
5
|
+
# automatically creates POST/PUT/DELETE actions
|
6
|
+
# and map them to corresponding methods of given resource,
|
7
|
+
# so sending a POST request to the controller
|
8
|
+
# resulting in creating new object of given resource.
|
9
|
+
# PUT/PATCH requests will update objects by given id.
|
10
|
+
# DELETE requests will delete objects by given id.
|
11
|
+
#
|
12
|
+
# @params [Class] resource
|
13
|
+
# @params [Array] path_or_opts
|
14
|
+
# @option path_or_opts [String || Array] :exclude
|
15
|
+
# sometimes forms sending extra params. exclude them using :exclude option
|
16
|
+
# @option path_or_opts [Integer] :pkey
|
17
|
+
# item's primary key(default :id) to be returned when item created /updated.
|
18
|
+
# if no pkey given and item does not respond to :id, the item itself returned.
|
19
|
+
# @option path_or_opts [Integer] :halt_on_errors
|
20
|
+
# if created/updated item contain errors,
|
21
|
+
# halt processing unconditionally, even if proc given.
|
22
|
+
# if this option is false(default) and a proc given,
|
23
|
+
# it will pass item object and extracted errors to proc rather than halt.
|
24
|
+
# @option path_or_opts [Integer] :halt_with
|
25
|
+
# when resource are not created/updated because of errors,
|
26
|
+
# Espresso will halt operation with 500 error status code.
|
27
|
+
# use :halt_with option to set a custom error status code.
|
28
|
+
# @option path_or_opts [String] :join_with
|
29
|
+
# if resource thrown some errors, Espresso will join them using a coma.
|
30
|
+
# use :join_with option to set a custom glue.
|
31
|
+
#
|
32
|
+
def crudify resource, *path_or_opts, &proc
|
33
|
+
opts = path_or_opts.last.is_a?(Hash) ? path_or_opts.pop : {}
|
34
|
+
if opts[:exclude]
|
35
|
+
opts[:exclude] = [ opts[:exclude] ] unless opts[:exclude].is_a?(Array)
|
36
|
+
else
|
37
|
+
opts[:exclude] = []
|
38
|
+
end
|
39
|
+
path = path_or_opts.first
|
40
|
+
action = '%s_' << (path || :index).to_s
|
41
|
+
orm = :ar if resource.respond_to?(:arel_table)
|
42
|
+
orm = :dm if resource.respond_to?(:storage_name)
|
43
|
+
orm = :sq if resource.respond_to?(:db_schema)
|
44
|
+
orm_map = {
|
45
|
+
ar: {
|
46
|
+
get: :find,
|
47
|
+
put: :update_attributes,
|
48
|
+
patch: :update_attributes
|
49
|
+
},
|
50
|
+
sq: {
|
51
|
+
get: :[]
|
52
|
+
}
|
53
|
+
}[orm] || {}
|
54
|
+
resource_method = {
|
55
|
+
get: opts.fetch(:get, orm_map[:get] || :get),
|
56
|
+
put: opts.fetch(:put, orm_map[:put] || :update),
|
57
|
+
post: opts.fetch(:post, :create),
|
58
|
+
patch: opts.fetch(:patch, orm_map[:patch] || :update),
|
59
|
+
delete: opts.fetch(:delete, :destroy),
|
60
|
+
}
|
61
|
+
|
62
|
+
proc_accept_object, proc_accept_errors = nil
|
63
|
+
if proc
|
64
|
+
proc_accept_object = proc.arity > 0
|
65
|
+
proc_accept_errors = proc.arity > 1
|
66
|
+
end
|
67
|
+
|
68
|
+
pkey = opts[:pkey] || :id
|
69
|
+
join_with = opts[:join_with] || ', '
|
70
|
+
halt_with = opts[:halt_with] || 500
|
71
|
+
|
72
|
+
presenter = lambda do |controller_instance, obj, err|
|
73
|
+
|
74
|
+
# extracting errors, if any
|
75
|
+
errors = nil
|
76
|
+
if err || (obj.respond_to?(:errors) && (err = obj.errors) &&
|
77
|
+
err.respond_to?(:size) && err.size > 0)
|
78
|
+
|
79
|
+
if err.respond_to?(:join)
|
80
|
+
errors = err
|
81
|
+
else
|
82
|
+
if err.respond_to?(:each_pair) # seems error is a Hash
|
83
|
+
# some objects may respond to `each_pair` but not respond to `inject`,
|
84
|
+
# so using trivial looping to extract error messages.
|
85
|
+
errors = []
|
86
|
+
err.each_pair do |k,v|
|
87
|
+
# usually DataMapper returns errors in the following format:
|
88
|
+
# { :property => ['error 1', 'error 2'] }
|
89
|
+
# flatten is here just in case we get nested arrays.
|
90
|
+
error = v.is_a?(Array) ? v.flatten.join(join_with) : v.to_s
|
91
|
+
errors << '%s: %s' % [k, error]
|
92
|
+
end
|
93
|
+
elsif err.respond_to?(:to_a) # not Array nor Hash, but convertible to Array
|
94
|
+
# converting error to Array and joining
|
95
|
+
errors = err.to_a
|
96
|
+
elsif err.is_a?(String)
|
97
|
+
errors = [err]
|
98
|
+
else
|
99
|
+
errors = [err.inspect]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
errors && errors = controller_instance.escape_html(errors.join(join_with))
|
104
|
+
|
105
|
+
if proc
|
106
|
+
if errors && opts[:halt_on_errors]
|
107
|
+
controller_instance.halt halt_with, errors
|
108
|
+
end
|
109
|
+
proc_args = []
|
110
|
+
proc_args = [obj] if proc_accept_object
|
111
|
+
proc_args = [obj, errors] if proc_accept_errors
|
112
|
+
controller_instance.instance_exec(*proc_args, &proc)
|
113
|
+
else
|
114
|
+
if errors
|
115
|
+
# no proc given, so halting when some errors occurring
|
116
|
+
controller_instance.styled_halt halt_with, errors
|
117
|
+
else
|
118
|
+
# no proc given and no errors detected,
|
119
|
+
# so extracting and returning object's pkey.
|
120
|
+
# if no pkey given and object does not respond to :id nor to :[],
|
121
|
+
# returning object as is.
|
122
|
+
if obj.respond_to?(:[]) && obj.respond_to?(:has_key?)
|
123
|
+
obj.has_key?(pkey) ? obj[pkey] : obj
|
124
|
+
elsif obj.respond_to?(pkey)
|
125
|
+
obj.send(pkey)
|
126
|
+
else
|
127
|
+
obj
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
fetch_object = lambda do |controller_instance, id|
|
134
|
+
obj, err = nil
|
135
|
+
begin
|
136
|
+
id = id.to_i if id =~ /\A\d+\Z/
|
137
|
+
obj = resource.send(resource_method[:get], id) ||
|
138
|
+
controller_instance.halt(404, 'object with ID %s not found' % controller_instance.escape_html(id))
|
139
|
+
rescue => e
|
140
|
+
err = e.message
|
141
|
+
end
|
142
|
+
[obj, err]
|
143
|
+
end
|
144
|
+
|
145
|
+
create_object = lambda do |controller_instance|
|
146
|
+
obj, err = nil
|
147
|
+
begin
|
148
|
+
data = controller_instance.params.reject { |k,v| opts[:exclude].include?(k) }
|
149
|
+
unless data.has_key?(E__CRUD__AUTH_TEST_PAYLOAD_KEY)
|
150
|
+
obj = resource.send(resource_method[:post], data)
|
151
|
+
end
|
152
|
+
rescue => e
|
153
|
+
err = e.message
|
154
|
+
end
|
155
|
+
[obj, err]
|
156
|
+
end
|
157
|
+
|
158
|
+
update_object = lambda do |controller_instance, request_method, id|
|
159
|
+
obj, err = nil
|
160
|
+
begin
|
161
|
+
obj, err = fetch_object.call(controller_instance, id)
|
162
|
+
unless err
|
163
|
+
data = controller_instance.params.reject { |k,v| opts[:exclude].include?(k) }
|
164
|
+
obj.send(resource_method[request_method], data)
|
165
|
+
end
|
166
|
+
rescue => e
|
167
|
+
err = e.message
|
168
|
+
end
|
169
|
+
[obj, err]
|
170
|
+
end
|
171
|
+
|
172
|
+
# fetching object by given id
|
173
|
+
# and calling #destroy(or whatever in options for delete) on it
|
174
|
+
#
|
175
|
+
# @return [String] empty string
|
176
|
+
delete_object = lambda do |id|
|
177
|
+
err = nil
|
178
|
+
begin
|
179
|
+
obj, err = fetch_object.call(self, id)
|
180
|
+
unless err
|
181
|
+
meth = resource_method[:delete]
|
182
|
+
if obj.respond_to?(meth)
|
183
|
+
obj.send(meth)
|
184
|
+
else
|
185
|
+
err = '%s does not respond to %s' % [obj.inspect, escape_html(meth)]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
rescue => e
|
189
|
+
err = e.message
|
190
|
+
end
|
191
|
+
[nil, err]
|
192
|
+
end
|
193
|
+
|
194
|
+
options = lambda do |controller_instance|
|
195
|
+
EConstants::HTTP__REQUEST_METHODS.reject do |rm|
|
196
|
+
next if rm == 'OPTIONS'
|
197
|
+
args = rm == 'POST' ?
|
198
|
+
[{E__CRUD__AUTH_TEST_PAYLOAD_KEY => 'true'}] :
|
199
|
+
[E__CRUD__AUTH_TEST_PAYLOAD_KEY]
|
200
|
+
s,h,b = controller_instance.invoke(action % rm.downcase, *args) do |env|
|
201
|
+
env.update EConstants::ENV__REQUEST_METHOD => rm
|
202
|
+
end
|
203
|
+
s == EConstants::STATUS__PROTECTED
|
204
|
+
end.join(', ')
|
205
|
+
end
|
206
|
+
|
207
|
+
self.class_exec do
|
208
|
+
|
209
|
+
define_method action % :get do |id|
|
210
|
+
presenter.call self, *fetch_object.call(self, id)
|
211
|
+
end
|
212
|
+
|
213
|
+
define_method action % :head do |id|
|
214
|
+
presenter.call self, *fetch_object.call(self, id)
|
215
|
+
end
|
216
|
+
|
217
|
+
define_method action % :post do
|
218
|
+
presenter.call self, *create_object.call(self)
|
219
|
+
end
|
220
|
+
|
221
|
+
[:put, :patch].each do |request_method|
|
222
|
+
define_method action % request_method do |id|
|
223
|
+
presenter.call self, *update_object.call(self, request_method, id)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
define_method action % :delete do |id|
|
228
|
+
presenter.call self, *delete_object.call(id)
|
229
|
+
end
|
230
|
+
|
231
|
+
define_method action % :options do
|
232
|
+
options.call self
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
alias crud crudify
|
238
|
+
|
239
|
+
end
|
data/lib/el/ipcm.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Inter-Process Cache Manager
|
2
|
+
class EBuilder
|
3
|
+
|
4
|
+
def ipcm_trigger *args
|
5
|
+
if pids_reader
|
6
|
+
pids = pids_reader.call rescue nil
|
7
|
+
if pids.is_a?(Array)
|
8
|
+
pids.map(&:to_i).reject {|p| p < 2 || p == Process.pid }.each do |pid|
|
9
|
+
file = '%s/%s.%s-%s' % [ipcm_tmpdir, pid, args.hash, Time.now.to_f]
|
10
|
+
begin
|
11
|
+
File.open(file, 'w') {|f| f << Marshal.dump(args)}
|
12
|
+
Process.kill ipcm_signal, pid
|
13
|
+
rescue => e
|
14
|
+
warn "was unable to perform IPCM operation because of error: %s" % ::CGI.escapeHTML(e.message)
|
15
|
+
File.unlink(file) if File.file?(file)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
else
|
19
|
+
warn "pids_reader should return an array of pids. Exiting IPCM..."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def ipcm_tmpdir path = nil
|
25
|
+
return @ipcm_tmpdir if @ipcm_tmpdir
|
26
|
+
if path
|
27
|
+
@ipcm_tmpdir = ((path =~ /\A\// ? path : root + path) + '/').freeze
|
28
|
+
else
|
29
|
+
@ipcm_tmpdir = (root + 'tmp/ipcm/').freeze
|
30
|
+
end
|
31
|
+
FileUtils.mkdir_p @ipcm_tmpdir
|
32
|
+
@ipcm_tmpdir
|
33
|
+
end
|
34
|
+
|
35
|
+
def ipcm_signal signal = nil
|
36
|
+
return @ipcm_signal if @ipcm_signal
|
37
|
+
@ipcm_signal = signal.to_s if signal
|
38
|
+
@ipcm_signal ||= 'ALRM'
|
39
|
+
end
|
40
|
+
|
41
|
+
def register_ipcm_signal
|
42
|
+
Signal.trap ipcm_signal do
|
43
|
+
Dir[ipcm_tmpdir + '%s.*' % Process.pid].each do |file|
|
44
|
+
unless (setup = Marshal.restore(File.read(file)) rescue nil).is_a?(Array)
|
45
|
+
warn "Was unable to process \"%s\" cache file, skipping cache cleaning" % file
|
46
|
+
end
|
47
|
+
File.unlink(file) if File.file?(file)
|
48
|
+
meth = setup.shift
|
49
|
+
[ :clear_cache,
|
50
|
+
:clear_cache_like,
|
51
|
+
:clear_compiler,
|
52
|
+
:clear_compiler_like,
|
53
|
+
].include?(meth) && self.send(meth, *setup)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def pids_reader &proc
|
59
|
+
return @pids_reader if @pids_reader
|
60
|
+
if proc.is_a?(Proc)
|
61
|
+
@pids_reader = proc
|
62
|
+
register_ipcm_signal
|
63
|
+
end
|
64
|
+
end
|
65
|
+
alias pids pids_reader
|
66
|
+
|
67
|
+
end
|