web-utils 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 406c990b91a61d30f76d4ff065013eaf1ec741ea
4
+ data.tar.gz: 4788cfe256f7e536de8067c79bef7193a35ed0e7
5
+ SHA512:
6
+ metadata.gz: 2a675233f99d1b6b7299e830c6d80fbf46637b2d9c43c1a35753887ec1f254b126f4e0cfcb439d080a1c11eb80bd527f0807c4030f352365d44644b23ddea476
7
+ data.tar.gz: 08f910f903304d445e32a66f20ad8bb9c025d6b5516f4437b83ae8f18654d0633d9c05b40275033b2cd2b451fc636ab41a0496e8ae978e4215fe86d69d18edb4
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .DS_STORE
2
+ *.swp
3
+ *.sass-cache
4
+ pkg/
5
+ Gemfile.lock
6
+ .bundle/
7
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+ gem 'minitest', '>=5.8.0'
4
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Mickael Riga
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
data/README.md ADDED
@@ -0,0 +1,242 @@
1
+ WebUtils
2
+ ========
3
+
4
+ `WebUtils` is basically a collection of useful helper
5
+ methods that are quite common to have in a web project.
6
+ It is organized like `Rack::Utils` and actually extends it.
7
+
8
+ Some of the methods are similar to methods you would have
9
+ in `Active::Support` but without monkey patching. Although it
10
+ is only a coincidence. The purpose is not to build an alternative.
11
+
12
+ Here is how you would use it with `Sinatra`:
13
+
14
+ ```ruby
15
+ require 'sinatra/base'
16
+ require 'web_utils'
17
+
18
+ class Main < Sinatra::Base
19
+
20
+ # Your frontend code...
21
+
22
+ helpers do
23
+ include WebUtils
24
+ # Your other helpers...
25
+ end
26
+
27
+ end
28
+ ```
29
+
30
+ Some methods are also useful on the model side.
31
+
32
+ Here is a list of the available methods and what they do:
33
+
34
+ `blank?(string)`
35
+ ----------------
36
+
37
+ Just tells you if the string is blank or not.
38
+
39
+ `pluralize(string)`
40
+ -------------------
41
+
42
+ Pluralize simple words. Just override when necessary.
43
+
44
+ `singularize(string)`
45
+ -------------------
46
+
47
+ Singularize simple words. Just override when necessary.
48
+
49
+ `dasherize_class_name(string)`
50
+ ------------------------------
51
+
52
+ Dasherize class names. Module separator is a double dash.
53
+ So `BlogArticle::Comment` becomes `blog-article--comment`.
54
+ Also for simplicity, it does not gather accronyms. e.g. `net--f-t-p`.
55
+ This is useful for urls or creating CSS class names or IDs.
56
+
57
+ `undasherize_class_name(string)`
58
+ --------------------------------
59
+
60
+ Basically the opposite.
61
+
62
+ `resolve_class_name(string, context=Kernel)`
63
+ --------------------------------------------
64
+
65
+ It takes the class name as a string and returns the class.
66
+ You can pass a class name with modules as well (e.g. "Net::FTP").
67
+ This is actually the main reason why there is a `context`
68
+ argument, because it uses recursion to do this.
69
+ But `context` is still useful otherwise.
70
+
71
+ `resolve_dasherized_class_name(string)`
72
+ ---------------------------------------
73
+
74
+ Same except that it takes the dasherized version of the
75
+ class name as an argument and returns the class itself (not a string).
76
+ Useful for resolving a class from a URL param.
77
+
78
+ `guess_related_class_name(parent_class, string)`
79
+ ------------------------------------------------
80
+
81
+ It is mainly used for guessing the class name of a
82
+ children class with a plural name.
83
+ So `guess_related_class_name(BlogArticle, :comments)`
84
+ will return `'BlogArticle::Comment'`.
85
+
86
+ `get_value(value, target=Kernel)`
87
+ ----------------------------------
88
+
89
+ It is used for example for getting a default value for something
90
+ and it is passed either as:
91
+
92
+ - A direct value (e.g. `"John Doe"`)
93
+ - A proc (e.g. `proc{ Time.now }`)
94
+ - A symbol (e.g. `:get_last_value`)
95
+
96
+ In the case of a symbol, the message is called on the target.
97
+ Therefore you would always give the target just in case it
98
+ is a symbol.
99
+
100
+ If the value can only be direct or a Proc, you can ignore the
101
+ second argument (target).
102
+
103
+ `deep_copy(object)`
104
+ -------------------
105
+
106
+ This makes a deeper copy of an object, since `dup` does not
107
+ duplicate nested objects in a hash for example. It uses a simple
108
+ marshal/unmarshal mechanism.
109
+
110
+ It is a bit of a hack and not that web-specific, but useful
111
+ if you want to avoid some nasty bugs.
112
+
113
+ `ensure_key!(hash, key, default_value)`
114
+ ---------------------------------------
115
+
116
+ If the hash does not have the key, it sets it with the
117
+ default value. And this value is also returned by the method.
118
+
119
+ `ensure_key(hash, key, default_value)`
120
+ --------------------------------------
121
+
122
+ Same as `ensure_key!` except that it does not change the original
123
+ hash. It returns a new one.
124
+
125
+ `slugify(string, force_lowercase=true)`
126
+ -----------------
127
+
128
+ This makes the strings ready to be used as a slug in a URL.
129
+ It removes the accents, replaces a lot of separators with
130
+ dashes and escapes it. By default it forces the output to
131
+ be lowercase, but if you pass `false` as a second argument,
132
+ it will not change the case of letters.
133
+
134
+ `label_for_field(string_or_symbol)`
135
+ -----------------------------------
136
+
137
+ Returns a human readable version of a field name.
138
+ It says `field`, but it could be any kind of symbol I guess.
139
+
140
+ `each_stub(nested_object) {|object,key_or_index,value| ... }`
141
+ -------------------------------------------------------------
142
+
143
+ It is used to run something on all the nested stubs of
144
+ an array or a hash. The second argument of the block is
145
+ either a key if the object is a hash, or an index if the
146
+ object is an array.
147
+
148
+ `automatic_typecast(string, casted=[:bool, :nil, :int, :float])`
149
+ ----------------------------
150
+
151
+ It tries to change a string value received by an HTML form
152
+ or a CSV file into an object when it can. So far it recognizes
153
+ simple things like `true`, `false`, integers and floats.
154
+ And an empty string is always `nil`.
155
+
156
+ The second argument is the list of things you want to typecast.
157
+ By default there is everything, but you only want to typecast
158
+ integers and floats, you can pass `[:int, :float]`.
159
+
160
+ `generate_random_id(size)`
161
+ --------------------------
162
+
163
+ Like the name suggests, it generates a random string of
164
+ only letters and numbers. If you don't provide a size,
165
+ it defaults to 16.
166
+
167
+ `nl2br(string, br="<br>")`
168
+ --------------------------
169
+
170
+ The classic `nl2br` which makes sure return lines are
171
+ turned into `<br>` tags. You can use the second argument if
172
+ you want to specify what the replacement tag should be.
173
+ Just in case you want self-closing tags.
174
+
175
+ `complete_link(string)`
176
+ -----------------------
177
+
178
+ This just makes sure that a link is complete. Very often
179
+ people tend to enter a URL like `www.google.com` which is a
180
+ controversial `href` for some browsers, so it changes it to
181
+ `//www.google.com`. Already seemingly complete links are not
182
+ affected by the method.
183
+
184
+ `external_link?(string)`
185
+ ------------------------
186
+
187
+ This tells you if a link is pointing to the current site or
188
+ an external one. This is useful when you want to create a link
189
+ tag and want to decide if target is `'_blank'` or `'_self'`.
190
+
191
+ `automatic_html(string, br="<br>")`
192
+ -----------------------------------
193
+
194
+ This automatically does `nl2br` and links recognizable things
195
+ like email addresses and URLs. Not as good as markdown, but it
196
+ is quite useful, should it be only for turning an email into a link.
197
+
198
+ `truncate(string, size=320, ellipsis="...")`
199
+ --------------------------------------------
200
+
201
+ It truncates a string like what you have in blog summaries.
202
+ It automatically removes tags and line breaks. The length is
203
+ 320 by default. When the original string was longer, it puts
204
+ an ellipsis at the end which can be replaced by whatever you put
205
+ as a 3rd argument. e.g. `'...and more'`.
206
+
207
+ `display_price(int)`
208
+ --------------------
209
+
210
+ It changes a price in cents/pence into a formated string
211
+ like `49,425.40` when you pass `4942540`.
212
+
213
+ `parse_price(string)`
214
+ ---------------------
215
+
216
+ It does the opposite of `display_price` and parses a string in
217
+ order to return a price in cents/pence.
218
+
219
+ `branded_filename(path, brand="WebUtils")`
220
+ ------------------------------------------
221
+
222
+ It takes the path to a file and add the brand/prefix and a dash
223
+ before the file name (really the file name, not the path).
224
+ By default, the brand/prefix is `WebUtils`.
225
+
226
+ `filename_variation(path, variation, ext)`
227
+ ------------------------------------------
228
+
229
+ For example you have a file `/path/to/image.jpg` and you want
230
+ to create its `thumbnail` in `png`, you can create the thumnail
231
+ path with `filename_variation(path, :thumbnail, :png)` and it
232
+ will return `/path/to/image.thumbnail.png`.
233
+
234
+ `initial_request?(request)`
235
+ ---------------------------
236
+
237
+ You basically pass the `Request` object to the method and it
238
+ looks at the referrer and returns true if it was not on the same
239
+ domain. Essentially tells you if the visitor just arrived.
240
+
241
+
242
+
data/lib/web_utils.rb ADDED
@@ -0,0 +1,232 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rack/utils'
4
+ require 'uri'
5
+
6
+ module WebUtils
7
+
8
+ # Most methods are supposed to be as simple as possible
9
+ # and just cover most cases.
10
+ # I would rather override specific cases rather than making
11
+ # complicated methods.
12
+
13
+ extend Rack::Utils
14
+
15
+ def blank? s
16
+ s.to_s.strip==''
17
+ end
18
+ module_function :blank?
19
+
20
+ def pluralize s
21
+ s<<'e' if s[-1,1]=='x'
22
+ s<<'s'
23
+ s.sub(/([b-df-hj-np-tv-z])ys$/,'\1ies')
24
+ end
25
+ module_function :pluralize
26
+
27
+ def singularize s
28
+ case s
29
+ when /xes$/
30
+ s[0..-3]
31
+ when /ies$/
32
+ s.sub(/ies$/, 'y')
33
+ when /s$/
34
+ s[0..-2]
35
+ else
36
+ s
37
+ end
38
+ end
39
+ module_function :singularize
40
+
41
+ def dasherize_class_name s
42
+ s.gsub(/([A-Z]|\d+)/){|str|"-#{str.downcase}"}[1..-1].gsub('::','-')
43
+ end
44
+ module_function :dasherize_class_name
45
+
46
+ def undasherize_class_name s
47
+ s.capitalize.gsub(/\-([a-z0-9])/){|str|$1.upcase}.gsub('-','::')
48
+ end
49
+ module_function :undasherize_class_name
50
+
51
+ def resolve_class_name s, context=Kernel
52
+ current, *payload = s.to_s.split('::')
53
+ raise(NameError) if current.nil?
54
+ const = context.const_get(current)
55
+ if payload.empty?
56
+ const
57
+ else
58
+ resolve_class_name(payload.join('::'),const)
59
+ end
60
+ end
61
+ module_function :resolve_class_name
62
+
63
+ def resolve_dasherized_class_name s
64
+ resolve_class_name(undasherize_class_name(s.to_s))
65
+ end
66
+ module_function :resolve_dasherized_class_name
67
+
68
+ def guess_related_class_name context, clue
69
+ context.respond_to?(:name) ? context.name : context.to_s
70
+ clue = clue.to_s
71
+ return clue if clue=~/^[A-Z]/
72
+ if clue=~/^[a-z]/
73
+ clue = undasherize_class_name singularize(clue).gsub('_','-')
74
+ clue = "::#{clue}"
75
+ end
76
+ "#{context}#{clue}"
77
+ end
78
+ module_function :guess_related_class_name
79
+
80
+ def get_value raw, context=Kernel
81
+ if raw.is_a? Proc
82
+ raw.call
83
+ elsif raw.is_a? Symbol
84
+ context.__send__ raw
85
+ else
86
+ raw
87
+ end
88
+ end
89
+ module_function :get_value
90
+
91
+ def deep_copy original
92
+ Marshal.load(Marshal.dump(original))
93
+ end
94
+ module_function :deep_copy
95
+
96
+ def ensure_key! h, k, v
97
+ h[k] = v unless h.key?(k)
98
+ h[k]
99
+ end
100
+ module_function :ensure_key!
101
+
102
+ def ensure_key h, k, v
103
+ new_h = h.dup
104
+ self.ensure_key! new_h, k, v
105
+ new_h
106
+ end
107
+ module_function :ensure_key
108
+
109
+ ACCENTS_FROM =
110
+ "ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞ"
111
+ ACCENTS_TO =
112
+ "AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssT"
113
+ def slugify s, force_lower=true
114
+ s = s.to_s.tr(ACCENTS_FROM,ACCENTS_TO).tr(' .,;:?!/\'"()[]{}<>','-').gsub(/&/, 'and').gsub(/-+/,'-').gsub(/(^-|-$)/,'')
115
+ s = s.downcase if force_lower
116
+ escape(s)
117
+ end
118
+ module_function :slugify
119
+
120
+ def label_for_field field_name
121
+ field_name.to_s.scan(/[a-zA-Z0-9]+/).map(&:capitalize).join(' ')
122
+ end
123
+ module_function :label_for_field
124
+
125
+ def each_stub obj, &block
126
+ raise TypeError, 'WebUtils.each_stub expects an object which respond to each_with_index' unless obj.respond_to?(:each_with_index)
127
+ obj.each_with_index do |(k,v),i|
128
+ value = v || k
129
+ if value.is_a?(Hash) || value.is_a?(Array)
130
+ each_stub(value,&block)
131
+ else
132
+ block.call(obj, (v.nil? ? i : k), value)
133
+ end
134
+ end
135
+ end
136
+ module_function :each_stub
137
+
138
+ def automatic_typecast str, casted=[:bool,:nil,:int,:float]
139
+ return str unless str.is_a?(String)
140
+ if casted.include?(:bool) and str=='true'
141
+ true
142
+ elsif casted.include?(:bool) and str=='false'
143
+ false
144
+ elsif casted.include?(:nil) and str==''
145
+ nil
146
+ elsif casted.include?(:int) and str=~/^-?\d+$/
147
+ str.to_i
148
+ elsif casted.include?(:float) and str=~/^-?\d*\.\d+$/
149
+ str.to_f
150
+ else
151
+ str
152
+ end
153
+ end
154
+ module_function :automatic_typecast
155
+
156
+ ID_CHARS = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
157
+ ID_SIZE = 16
158
+ def generate_random_id size=ID_SIZE
159
+ id = ''
160
+ size.times{id << ID_CHARS[rand(ID_CHARS.size)]}
161
+ id
162
+ end
163
+ module_function :generate_random_id
164
+
165
+ def nl2br s, br='<br>'
166
+ s.to_s.gsub(/\n/,br)
167
+ end
168
+ module_function :nl2br
169
+
170
+ def complete_link link
171
+ if blank?(link) or link=~/^(\/|[a-z]*:)/
172
+ link
173
+ else
174
+ "//#{link}"
175
+ end
176
+ end
177
+ module_function :complete_link
178
+
179
+ def external_link? link
180
+ !!(link =~ /^[a-z]*:?\/\//)
181
+ end
182
+ module_function :external_link?
183
+
184
+ def automatic_html s, br='<br>'
185
+ replaced = s.to_s.
186
+ gsub(/\b((https?:\/\/|ftps?:\/\/|www\.)([A-Za-z0-9\-_=%&@\?\.\/]+))\b/) do |str|
187
+ url = complete_link $1
188
+ "<a href='#{url}' target='_blank'>#{$1}</a>"
189
+ end.
190
+ gsub(/([^\s]+@[^\s]*[a-zA-Z])/) do |str|
191
+ "<a href='mailto:#{$1.downcase}'>#{$1}</a>"
192
+ end
193
+ nl2br(replaced,br).gsub("@", "&#64;")
194
+ end
195
+ module_function :automatic_html
196
+
197
+ def truncate s,c=320,ellipsis='...'
198
+ s.to_s.gsub(/<[^>]*>/, '').gsub(/\n/, ' ').sub(/^(.{#{c}}\w*).*$/m, '\1'+ellipsis)
199
+ end
200
+ module_function :truncate
201
+
202
+ def display_price int
203
+ raise(TypeError, 'The price needs to be the price in cents/pence as an integer') unless int.is_a?(Integer)
204
+ ("%.2f" % (int/100.0)).sub(/\.00/, '').reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
205
+ end
206
+ module_function :display_price
207
+
208
+ def parse_price string
209
+ raise(TypeError, 'The price needs to be parsed from a String') unless string.is_a?(String)
210
+ ("%.2f" % string.gsub(/[^\d\.\-]/, '')).gsub(/\./,'').to_i
211
+ end
212
+ module_function :parse_price
213
+
214
+ def branded_filename path, brand='WebUtils'
215
+ "#{File.dirname(path)}/#{brand}-#{File.basename(path)}".sub(/^\.\//,'')
216
+ end
217
+ module_function :branded_filename
218
+
219
+ def filename_variation path, variation, ext
220
+ old_ext = File.extname(path)
221
+ path.sub(/#{Regexp.escape old_ext}$/, ".#{variation}.#{ext}")
222
+ end
223
+ module_function :filename_variation
224
+
225
+ def initial_request? request
226
+ return true unless request.referer=~URI.regexp
227
+ URI.parse(request.referer).host!=request.host
228
+ end
229
+ module_function :initial_request?
230
+
231
+ end
232
+
@@ -0,0 +1,524 @@
1
+ require 'minitest/autorun'
2
+ require 'web_utils'
3
+
4
+ require 'rack/request'
5
+ require 'rack/mock'
6
+
7
+ module FakeModule
8
+ module FakeSubModule
9
+ end
10
+ end
11
+
12
+ describe WebUtils do
13
+
14
+ parallelize_me!
15
+
16
+ let(:utils) { WebUtils }
17
+
18
+ describe '#blank?' do
19
+ describe 'with blank strings' do
20
+ it 'is true' do
21
+ ['',' '," \n \t"].each do |s|
22
+ assert utils.blank?(s)
23
+ end
24
+ end
25
+ end
26
+ describe 'with nil' do
27
+ it('is true') { assert utils.blank?(nil) }
28
+ end
29
+ describe 'with non-blank strings' do
30
+ it 'is false' do
31
+ ['a','abc', ' abc '].each do |s|
32
+ refute utils.blank?(s)
33
+ end
34
+ end
35
+ end
36
+ describe 'with integer' do
37
+ it('is false') { refute utils.blank?(1234) }
38
+ end
39
+ end
40
+
41
+ describe '#pluralize' do
42
+
43
+ it "Just adds an 's' at the end" do
44
+ assert_equal 'bags', utils.pluralize('bag')
45
+ assert_equal 'days', utils.pluralize('day')
46
+ end
47
+ describe "The word ends with 'x'" do
48
+ it "Adds 'es' instead" do
49
+ assert_equal 'foxes', utils.pluralize('fox')
50
+ end
51
+ end
52
+ describe "The word ends with a consonant and 'y'" do
53
+ it "Replaces 'y' with 'ie'" do
54
+ assert_equal 'copies', utils.pluralize('copy')
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ describe '#singularize' do
61
+
62
+ it "Removes the trailing 's'" do
63
+ assert_equal 'bag', utils.singularize('bags')
64
+ end
65
+ describe "The word ends with 'xes'" do
66
+ it "Removes the 'e' as well" do
67
+ assert_equal 'fox', utils.singularize('foxes')
68
+ end
69
+ end
70
+ describe "The word ends with 'ies'" do
71
+ it "Replaces 'ie' with 'y'" do
72
+ assert_equal 'copy', utils.singularize('copies')
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ describe 'dasherize/undasherize class name' do
79
+
80
+ let(:cases) {
81
+ [
82
+ ['Hello','hello'],
83
+ ['HelloWorld','hello-world'],
84
+ ['HTTPRequest','h-t-t-p-request'],
85
+ ['RestAPI','rest-a-p-i'],
86
+ ['AlteredSMTPAttachment','altered-s-m-t-p-attachment'],
87
+ ['View360', 'view-360'],
88
+ ['View360Degree', 'view-360-degree'],
89
+ ['Degree360::View', 'degree-360--view'],
90
+ ['RestAPI::Request::Post','rest-a-p-i--request--post'],
91
+ ]
92
+ }
93
+
94
+ describe '#dasherize_class_name' do
95
+ it "Translates correctly" do
96
+ cases.each do |(classname,dashname)|
97
+ assert_equal dashname, utils.dasherize_class_name(classname)
98
+ end
99
+ end
100
+ end
101
+
102
+ describe '#undasherize_class_name' do
103
+ it "Translates correctly" do
104
+ cases.each do |(classname,dashname)|
105
+ assert_equal classname, utils.undasherize_class_name(dashname)
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ describe '#resolve_class_name' do
113
+ describe 'when the constant exists' do
114
+ it 'Returns the constant' do
115
+ [
116
+ ['String',String],
117
+ ['FakeModule::FakeSubModule',FakeModule::FakeSubModule],
118
+ ].each do |(classname,constant)|
119
+ assert_equal constant, utils.resolve_class_name(classname)
120
+ end
121
+ end
122
+ end
123
+ describe 'when the constant does not exist' do
124
+ it 'Raise if the constant does not exist' do
125
+ ['Strang','WebUtils::Yootils','',nil].each do |classname|
126
+ assert_raises(NameError) do
127
+ utils.resolve_class_name(classname)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ describe '#resolve_dasherized_class_name' do
135
+ it 'Chains both methods for sugar' do
136
+ assert_equal FakeModule::FakeSubModule, utils.resolve_dasherized_class_name('fake-module--fake-sub-module')
137
+ assert_equal FakeModule::FakeSubModule, utils.resolve_dasherized_class_name(:'fake-module--fake-sub-module')
138
+ end
139
+ end
140
+
141
+ describe '#guess_related_class_name' do
142
+
143
+ # Used for many things but mainly relationship classes
144
+
145
+ describe 'when it starts with a lowercase letter' do
146
+ it 'Should guess a singular class_name in the context' do
147
+ assert_equal 'FakeModule::FakeSubModule::RelatedThing', utils.guess_related_class_name(FakeModule::FakeSubModule, :related_things)
148
+ assert_equal 'FakeModule::FakeSubModule::RelatedThing', utils.guess_related_class_name(FakeModule::FakeSubModule, :related_thing)
149
+ end
150
+ end
151
+ describe 'when it starts with an uppercase letter' do
152
+ it 'Should return the class_name as-is' do
153
+ assert_equal 'Class::Given', utils.guess_related_class_name(FakeModule::FakeSubModule, 'Class::Given')
154
+ end
155
+ end
156
+ describe 'when it starts with ::' do
157
+ it 'Should prepend the class_name whith the context' do
158
+ assert_equal 'FakeModule::FakeSubModule::RelatedThing', utils.guess_related_class_name(FakeModule::FakeSubModule, '::RelatedThing')
159
+ end
160
+ end
161
+ end
162
+
163
+ describe '#get_value' do
164
+ describe 'when arg is a simple object' do
165
+ it 'Returns it as-is' do
166
+ assert_equal 'Hello', utils.get_value('Hello')
167
+ end
168
+ end
169
+ describe 'when arg is a proc' do
170
+ it 'Returns after calling the proc' do
171
+ assert_equal 'Hello', utils.get_value(proc{'Hello'})
172
+ end
173
+ end
174
+ describe 'when arg is a lambda' do
175
+ it 'Returns after calling the lambda' do
176
+ assert_equal 'Hello', utils.get_value(lambda{'Hello'})
177
+ end
178
+ end
179
+ describe 'when arg is a symbol' do
180
+ describe 'and a context is passed as a second argument' do
181
+ it 'Sends the message to the context' do
182
+ assert_equal 'Hello', utils.get_value(:capitalize,'hello')
183
+ end
184
+ end
185
+ describe 'and no context is passed' do
186
+ it 'Sends the message to Kernel' do
187
+ assert_equal 'Kernel', utils.get_value(:to_s)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ describe '#deep_copy' do
194
+ it 'Duplicates the nested objects' do
195
+ original = {nested_hash: {one: 1}, nested_array: [1]}
196
+ copy = utils.deep_copy(original)
197
+ copy[:nested_hash][:one] = 2
198
+ copy[:nested_array] << 2
199
+ assert_equal({one: 1}, original[:nested_hash])
200
+ assert_equal [1], original[:nested_array]
201
+ end
202
+ end
203
+
204
+ describe '#ensure_key!' do
205
+ let(:arg) { {a: 3} }
206
+ it 'Sets the key if it did not exist' do
207
+ utils.ensure_key!(arg,:b,4)
208
+ assert_equal 4, arg[:b]
209
+ end
210
+ it 'Leaves the key untouched if it already existed' do
211
+ utils.ensure_key!(arg,:a,4)
212
+ assert_equal 3, arg[:a]
213
+ end
214
+ it 'Returns the value of the key' do
215
+ assert_equal 4, utils.ensure_key!(arg,:b,4)
216
+ assert_equal 3, utils.ensure_key!(arg,:a,4)
217
+ end
218
+ end
219
+
220
+ describe '#ensure_key' do
221
+ let(:arg) { {a: 3} }
222
+ it 'Does not change the original' do
223
+ new_hash = utils.ensure_key(arg,:b,4)
224
+ refute_equal 4, arg[:b]
225
+ assert_equal 4, new_hash[:b]
226
+ end
227
+ end
228
+
229
+ describe '#slugify' do
230
+
231
+ # For making slug for a document
232
+ # Possibly used instead of the id
233
+
234
+ let(:arg) { "Así es la vida by Daniel Bär & Mickaël ? (100%)" }
235
+ it 'Builds a string made of lowercase URL-friendly chars' do
236
+ assert_equal 'asi-es-la-vida-by-daniel-bar-and-mickael-100%25', utils.slugify(arg)
237
+ end
238
+ describe 'when second argument is false' do
239
+ it 'Does not force to lowercase' do
240
+ assert_equal 'Asi-es-la-vida-by-Daniel-Bar-and-Mickael-100%25', utils.slugify(arg,false)
241
+ end
242
+ end
243
+ describe 'when argument is nil' do
244
+ let(:arg) { nil }
245
+ it 'Does not break' do
246
+ assert_equal '', utils.slugify(arg)
247
+ assert_equal '', utils.slugify(arg,false)
248
+ end
249
+ end
250
+ end
251
+
252
+ describe '#label_for_field' do
253
+
254
+ # Returns a friendly name for a field name
255
+
256
+ it 'Returns an ideal title case version of the field name' do
257
+ [
258
+ ['hello', 'Hello'],
259
+ ['hello-world_1234', 'Hello World 1234'],
260
+ [:hello_world, 'Hello World'],
261
+ ].each do |(arg,result)|
262
+ assert_equal result, utils.label_for_field(arg)
263
+ end
264
+ end
265
+ end
266
+
267
+ describe '#each_stub' do
268
+
269
+ # For iterating through end objects of a nested hash/array
270
+ # It would be used for updating values, typecasting them...
271
+
272
+ it 'Yields a block for every stub of a complex object and make changes possible' do
273
+ before = {
274
+ 'name'=>"BoBBy",
275
+ 'numbers'=>['One','Two'],
276
+ 'meta'=>{'type'=>'Dev','tags'=>['Top','Bottom']}
277
+ }
278
+ after = {
279
+ 'name'=>"bobby",
280
+ 'numbers'=>['one','two'],
281
+ 'meta'=>{'type'=>'dev','tags'=>['top','bottom']}
282
+ }
283
+ utils.each_stub(before) do |object,key_index,value|
284
+ object[key_index] = value.to_s.downcase
285
+ end
286
+ assert_equal after, before
287
+ end
288
+ it 'Raises a TypeError if The object is not appropriate' do
289
+ [nil,'yo',4].each do |obj|
290
+ assert_raises(TypeError) do
291
+ utils.each_stub(obj)
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ describe '#automatic_typecast' do
298
+
299
+ # Tries to do automatic typecasting of values
300
+ # that are received as strings, so most likely
301
+ # coming from a web form or from a CSV table.
302
+ #
303
+ # The purpose is to use it in combination with
304
+ # #each_stub when a form is received by the API.
305
+
306
+ describe 'when a string' do
307
+ it 'Knows how to convert recognizable datatypes' do
308
+ [
309
+ ['true',true],
310
+ ['false',false],
311
+ ['',nil],
312
+ ['fack','fack'],
313
+ ['5', 5],
314
+ ['-5', -5],
315
+ ['42', 42],
316
+ ['-42', -42],
317
+ ['99.99', 99.99],
318
+ ['5.0', 5.0],
319
+ ['.4', 0.4],
320
+ ['-99.99', -99.99],
321
+ ['-5.0', -5.0],
322
+ ['-.4', -0.4],
323
+ ['42hello', '42hello'],
324
+ ['5.0hello', '5.0hello']
325
+ ].each do |(arg,result)|
326
+ assert_equal result, utils.automatic_typecast(arg)
327
+ end
328
+ end
329
+ it 'Can change what is typecasted' do
330
+ assert_equal '10', utils.automatic_typecast('10', [:bool,:nil])
331
+ assert_equal true, utils.automatic_typecast('true', [:bool,:nil])
332
+ end
333
+ end
334
+ describe 'when not a string' do
335
+ it 'Should leave it untouched' do
336
+ [Time.now,1.0].each do |obj|
337
+ assert_equal obj, utils.automatic_typecast(obj)
338
+ end
339
+ end
340
+ end
341
+ end
342
+
343
+ describe '#generate_random_id' do
344
+ it 'Has the correct format' do
345
+ assert_match /[a-zA-Z0-9]{16}/, utils.generate_random_id
346
+ end
347
+ it 'Can have a specific length' do
348
+ assert_match /[a-zA-Z0-9]{32}/, utils.generate_random_id(32)
349
+ end
350
+ end
351
+
352
+ describe '#nl2br' do
353
+ it 'Puts unclosed tags by default' do
354
+ assert_equal '<br>Hello<br>world<br>', utils.nl2br("\nHello\nworld\n")
355
+ end
356
+ describe 'with 2nd argument' do
357
+ it 'Replaces the tag' do
358
+ assert_equal '<br/>Hello<br/>world<br/>', utils.nl2br("\nHello\nworld\n",'<br/>')
359
+ end
360
+ end
361
+ end
362
+
363
+ describe '#complete_link' do
364
+ it 'Adds the external double slash when missing' do
365
+ [
366
+ ['www.web-utils.com','//www.web-utils.com'],
367
+ ['web-utils.com','//web-utils.com'],
368
+ ['please.web-utils.com','//please.web-utils.com'],
369
+ ].each do |(arg,result)|
370
+ assert_equal result, utils.complete_link(arg)
371
+ end
372
+ end
373
+ it 'Does not alter the url when it does not need double slash' do
374
+ [
375
+ ['//www.web-utils.com','//www.web-utils.com'],
376
+ ['://www.web-utils.com','://www.web-utils.com'],
377
+ ['http://www.web-utils.com','http://www.web-utils.com'],
378
+ ['ftp://www.web-utils.com','ftp://www.web-utils.com'],
379
+ ['mailto:web&#64;utils.com','mailto:web&#64;utils.com'],
380
+ ['',''],
381
+ [' ',' '],
382
+ ].each do |(arg,result)|
383
+ assert_equal result, utils.complete_link(arg)
384
+ end
385
+ end
386
+ end
387
+
388
+ describe '#external_link?' do
389
+ it 'Returns true when the link would need target=blank' do
390
+ [
391
+ ['http://web-utils.com', true],
392
+ ['https://web-utils.com', true],
393
+ ['ftp://web-utils.com', true],
394
+ ['://web-utils.com', true],
395
+ ['//web-utils.com', true],
396
+ ['mailto:user@web-utils.com', false],
397
+ ['mailto:user&#64;web-utils.com', false],
398
+ ['/web/utils', false],
399
+ ['web-utils.html', false],
400
+ ].each do |(url,bool)|
401
+ assert_equal bool, utils.external_link?(url)
402
+ end
403
+ end
404
+ end
405
+
406
+ describe '#automatic_html' do
407
+ it 'Automates links and line breaks' do
408
+ input = "Hello\nme@site.co.uk\nNot the begining me@site.co.uk\nme@site.co.uk not the end\nwww.site.co.uk\nVisit www.site.co.uk\nwww.site.co.uk rules\nhttp://www.site.co.uk\nVisit http://www.site.co.uk\nhttp://www.site.co.uk rules"
409
+ output = "Hello<br><a href='mailto:me&#64;site.co.uk'>me&#64;site.co.uk</a><br>Not the begining <a href='mailto:me&#64;site.co.uk'>me&#64;site.co.uk</a><br><a href='mailto:me&#64;site.co.uk'>me&#64;site.co.uk</a> not the end<br><a href='//www.site.co.uk' target='_blank'>www.site.co.uk</a><br>Visit <a href='//www.site.co.uk' target='_blank'>www.site.co.uk</a><br><a href='//www.site.co.uk' target='_blank'>www.site.co.uk</a> rules<br><a href='http://www.site.co.uk' target='_blank'>http://www.site.co.uk</a><br>Visit <a href='http://www.site.co.uk' target='_blank'>http://www.site.co.uk</a><br><a href='http://www.site.co.uk' target='_blank'>http://www.site.co.uk</a> rules"
410
+ assert_equal output, utils.automatic_html(input)
411
+ end
412
+ end
413
+
414
+ describe '#truncate' do
415
+ it 'Truncates to the right amount of letters' do
416
+ assert_equal 'abc...', utils.truncate('abc defg hijklmnopqrstuvwxyz',3)
417
+ end
418
+ it 'Does not cut words' do
419
+ assert_equal 'abcdefg...', utils.truncate('abcdefg hijklmnopqrstuvwxyz',3)
420
+ end
421
+ it 'Removes HTML tags' do
422
+ assert_equal 'abcdefg...', utils.truncate('<br>abc<a href=#>def</a>g hijklmnopqrstuvwxyz',3)
423
+ end
424
+ it 'Does not print the ellipsis if the string is already short enough' do
425
+ assert_equal 'abc def', utils.truncate('abc def',50)
426
+ end
427
+ describe 'with a 3rd argument' do
428
+ it 'Replaces the ellipsis' do
429
+ assert_equal 'abc!', utils.truncate('abc defg hijklmnopqrstuvwxyz',3,'!')
430
+ assert_equal 'abc', utils.truncate('abc defg hijklmnopqrstuvwxyz',3,'')
431
+ end
432
+ end
433
+ end
434
+
435
+ describe '#display_price' do
436
+ it 'Turns a price number in cents/pence into a displayable one' do
437
+ assert_equal '45.95', utils.display_price(4595)
438
+ end
439
+ it 'Removes cents if it is 00' do
440
+ assert_equal '70', utils.display_price(7000)
441
+ end
442
+ it 'Adds comma delimiters on thousands' do
443
+ assert_equal '12,345,678.90', utils.display_price(1234567890)
444
+ end
445
+ it 'Works with negative numbers' do
446
+ assert_equal '-1,400', utils.display_price(-140000)
447
+ end
448
+ it 'Raises when argument is not int' do
449
+ assert_raises(TypeError) do
450
+ utils.display_price('abc')
451
+ end
452
+ end
453
+ end
454
+
455
+ describe '#parse_price' do
456
+ it 'Parses a string and find the price in cents/pence' do
457
+ assert_equal 4595, utils.parse_price('45.95')
458
+ end
459
+ it 'Works when you omit the cents/pence' do
460
+ assert_equal 2800, utils.parse_price('28')
461
+ end
462
+ it 'Ignores visual help but works with negative prices' do
463
+ assert_equal -1234567890, utils.parse_price(' £-12,345,678.90 ')
464
+ end
465
+ it 'Raises when argument is not string' do
466
+ assert_raises(TypeError) do
467
+ utils.parse_price(42)
468
+ end
469
+ end
470
+ end
471
+
472
+ describe '#branded_filename' do
473
+ it 'Adds WebUtils to the file name' do
474
+ assert_equal "/path/to/WebUtils-file.png", utils.branded_filename("/path/to/file.png")
475
+ end
476
+ it 'Works when there is just a file name' do
477
+ assert_equal "WebUtils-file.png", utils.branded_filename("file.png")
478
+ end
479
+ it 'Can change the brand' do
480
+ assert_equal "/path/to/Brand-file.png", utils.branded_filename("/path/to/file.png",'Brand')
481
+ end
482
+ end
483
+
484
+ describe '#filename_variation' do
485
+ it 'Replaces the ext with variation name and new ext' do
486
+ assert_equal "/path/to/file.thumb.gif", utils.filename_variation("/path/to/file.png", :thumb, :gif)
487
+ end
488
+ it 'Works when there is just a filename' do
489
+ assert_equal "file.thumb.gif", utils.filename_variation("file.png", :thumb, :gif)
490
+ end
491
+ it "Works when there is no ext to start with" do
492
+ assert_equal "/path/to/file.thumb.gif", utils.filename_variation("/path/to/file", :thumb, :gif)
493
+ end
494
+ end
495
+
496
+ describe '#initial_request?' do
497
+ let(:req) {
498
+ Rack::Request.new(
499
+ Rack::MockRequest.env_for(
500
+ '/path',
501
+ {'HTTP_REFERER'=>referer}
502
+ )
503
+ )
504
+ }
505
+ let(:referer) { nil }
506
+ it 'Returns true' do
507
+ assert utils.initial_request?(req)
508
+ end
509
+ describe 'Request comes from another domain' do
510
+ let(:referer) { 'https://www.google.com/path' }
511
+ it 'Returns true' do
512
+ assert utils.initial_request?(req)
513
+ end
514
+ end
515
+ describe 'Request comes from same domain' do
516
+ let(:referer) { 'http://example.org' }
517
+ it 'Returns false' do
518
+ refute utils.initial_request?(req)
519
+ end
520
+ end
521
+ end
522
+
523
+ end
524
+
data/web-utils.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.authors = ["Mickael Riga"]
4
+ s.email = ["mig@mypeplum.com"]
5
+ s.homepage = "https://github.com/mig-hub/web-utils"
6
+ s.licenses = ['MIT']
7
+
8
+ s.name = 'web-utils'
9
+ s.version = '0.0.1'
10
+ s.summary = "Web Utils"
11
+ s.description = "Useful web-related helper methods for models, views or controllers."
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+ s.files = `git ls-files`.split("\n").sort
15
+ s.test_files = s.files.grep(/^test\//)
16
+ s.require_paths = ['lib']
17
+ s.add_dependency('rack', '~> 0')
18
+
19
+ end
20
+
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: web-utils
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mickael Riga
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Useful web-related helper methods for models, views or controllers.
28
+ email:
29
+ - mig@mypeplum.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - Gemfile
36
+ - LICENSE
37
+ - README.md
38
+ - lib/web_utils.rb
39
+ - test/test_web_utils.rb
40
+ - web-utils.gemspec
41
+ homepage: https://github.com/mig-hub/web-utils
42
+ licenses:
43
+ - MIT
44
+ metadata: {}
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 2.5.1
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Web Utils
65
+ test_files:
66
+ - test/test_web_utils.rb