apes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +82 -0
  4. data/.travis-gemfile +15 -0
  5. data/.travis.yml +15 -0
  6. data/.yardopts +1 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +22 -0
  9. data/README.md +177 -0
  10. data/Rakefile +44 -0
  11. data/apes.gemspec +34 -0
  12. data/doc/Apes.html +130 -0
  13. data/doc/Apes/Concerns.html +127 -0
  14. data/doc/Apes/Concerns/Errors.html +1089 -0
  15. data/doc/Apes/Concerns/Pagination.html +636 -0
  16. data/doc/Apes/Concerns/Request.html +766 -0
  17. data/doc/Apes/Concerns/Response.html +940 -0
  18. data/doc/Apes/Controller.html +1100 -0
  19. data/doc/Apes/Errors.html +125 -0
  20. data/doc/Apes/Errors/AuthenticationError.html +133 -0
  21. data/doc/Apes/Errors/BadRequestError.html +157 -0
  22. data/doc/Apes/Errors/BaseError.html +320 -0
  23. data/doc/Apes/Errors/InvalidDataError.html +157 -0
  24. data/doc/Apes/Errors/MissingDataError.html +157 -0
  25. data/doc/Apes/Model.html +378 -0
  26. data/doc/Apes/PaginationCursor.html +2138 -0
  27. data/doc/Apes/RuntimeConfiguration.html +909 -0
  28. data/doc/Apes/Serializers.html +125 -0
  29. data/doc/Apes/Serializers/JSON.html +389 -0
  30. data/doc/Apes/Serializers/JWT.html +452 -0
  31. data/doc/Apes/Serializers/List.html +347 -0
  32. data/doc/Apes/UrlsParser.html +1432 -0
  33. data/doc/Apes/Validators.html +125 -0
  34. data/doc/Apes/Validators/BaseValidator.html +278 -0
  35. data/doc/Apes/Validators/BooleanValidator.html +494 -0
  36. data/doc/Apes/Validators/EmailValidator.html +350 -0
  37. data/doc/Apes/Validators/PhoneValidator.html +375 -0
  38. data/doc/Apes/Validators/ReferenceValidator.html +372 -0
  39. data/doc/Apes/Validators/TimestampValidator.html +640 -0
  40. data/doc/Apes/Validators/UuidValidator.html +372 -0
  41. data/doc/Apes/Validators/ZipCodeValidator.html +372 -0
  42. data/doc/Apes/Version.html +189 -0
  43. data/doc/ApplicationController.html +547 -0
  44. data/doc/Concerns.html +128 -0
  45. data/doc/Concerns/ErrorHandling.html +826 -0
  46. data/doc/Concerns/PaginationHandling.html +463 -0
  47. data/doc/Concerns/RequestHandling.html +512 -0
  48. data/doc/Concerns/ResponseHandling.html +579 -0
  49. data/doc/Errors.html +126 -0
  50. data/doc/Errors/AuthenticationError.html +123 -0
  51. data/doc/Errors/BadRequestError.html +147 -0
  52. data/doc/Errors/BaseError.html +289 -0
  53. data/doc/Errors/InvalidDataError.html +147 -0
  54. data/doc/Errors/MissingDataError.html +147 -0
  55. data/doc/Model.html +315 -0
  56. data/doc/PaginationCursor.html +764 -0
  57. data/doc/Serializers.html +126 -0
  58. data/doc/Serializers/JSON.html +253 -0
  59. data/doc/Serializers/JWT.html +253 -0
  60. data/doc/Serializers/List.html +245 -0
  61. data/doc/Validators.html +126 -0
  62. data/doc/Validators/BaseValidator.html +209 -0
  63. data/doc/Validators/BooleanValidator.html +391 -0
  64. data/doc/Validators/EmailValidator.html +298 -0
  65. data/doc/Validators/PhoneValidator.html +313 -0
  66. data/doc/Validators/ReferenceValidator.html +284 -0
  67. data/doc/Validators/TimestampValidator.html +476 -0
  68. data/doc/Validators/UuidValidator.html +310 -0
  69. data/doc/Validators/ZipCodeValidator.html +310 -0
  70. data/doc/_index.html +435 -0
  71. data/doc/class_list.html +58 -0
  72. data/doc/css/common.css +1 -0
  73. data/doc/css/full_list.css +57 -0
  74. data/doc/css/style.css +339 -0
  75. data/doc/file.README.html +252 -0
  76. data/doc/file_list.html +60 -0
  77. data/doc/frames.html +26 -0
  78. data/doc/index.html +252 -0
  79. data/doc/js/app.js +219 -0
  80. data/doc/js/full_list.js +181 -0
  81. data/doc/js/jquery.js +4 -0
  82. data/doc/method_list.html +615 -0
  83. data/doc/top-level-namespace.html +112 -0
  84. data/lib/apes.rb +40 -0
  85. data/lib/apes/concerns/errors.rb +111 -0
  86. data/lib/apes/concerns/pagination.rb +81 -0
  87. data/lib/apes/concerns/request.rb +237 -0
  88. data/lib/apes/concerns/response.rb +74 -0
  89. data/lib/apes/controller.rb +77 -0
  90. data/lib/apes/errors.rb +38 -0
  91. data/lib/apes/model.rb +94 -0
  92. data/lib/apes/pagination_cursor.rb +152 -0
  93. data/lib/apes/runtime_configuration.rb +80 -0
  94. data/lib/apes/serializers.rb +88 -0
  95. data/lib/apes/urls_parser.rb +233 -0
  96. data/lib/apes/validators.rb +234 -0
  97. data/lib/apes/version.rb +24 -0
  98. data/spec/apes/concerns/errors_spec.rb +141 -0
  99. data/spec/apes/concerns/pagination_spec.rb +114 -0
  100. data/spec/apes/concerns/request_spec.rb +244 -0
  101. data/spec/apes/concerns/response_spec.rb +79 -0
  102. data/spec/apes/controller_spec.rb +54 -0
  103. data/spec/apes/errors_spec.rb +14 -0
  104. data/spec/apes/models_spec.rb +148 -0
  105. data/spec/apes/pagination_cursor_spec.rb +113 -0
  106. data/spec/apes/runtime_configuration_spec.rb +100 -0
  107. data/spec/apes/serializers_spec.rb +70 -0
  108. data/spec/apes/urls_parser_spec.rb +150 -0
  109. data/spec/apes/validators_spec.rb +237 -0
  110. data/spec/spec_helper.rb +30 -0
  111. data/views/_included.json.jbuilder +9 -0
  112. data/views/_pagination.json.jbuilder +9 -0
  113. data/views/collection.json.jbuilder +4 -0
  114. data/views/errors/400.json.jbuilder +9 -0
  115. data/views/errors/403.json.jbuilder +7 -0
  116. data/views/errors/404.json.jbuilder +6 -0
  117. data/views/errors/422.json.jbuilder +19 -0
  118. data/views/errors/500.json.jbuilder +12 -0
  119. data/views/errors/501.json.jbuilder +7 -0
  120. data/views/layouts/general.json.jbuilder +36 -0
  121. data/views/object.json.jbuilder +4 -0
  122. metadata +262 -0
@@ -0,0 +1,88 @@
1
+ #
2
+ # This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
3
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
4
+ #
5
+
6
+ module Apes
7
+ # A set of common serializers.
8
+ module Serializers
9
+ # Comma separated serialized value.
10
+ class List
11
+ # Loads serialized data.
12
+ #
13
+ # @param data [String] The serialized data.
14
+ # @return [Array] A array of values.
15
+ def self.load(data)
16
+ return data if data.is_a?(Array)
17
+ data.ensure_string.tokenize
18
+ end
19
+
20
+ # Serializes data.
21
+ #
22
+ # @param data [Object] The data to serialize.
23
+ # @return [String] Serialized data.
24
+ def self.dump(data)
25
+ data.ensure_array.compact.map(&:to_s).join(",")
26
+ end
27
+ end
28
+
29
+ # JSON encoded serialized value.
30
+ class JSON
31
+ # Saves serialized data.
32
+ #
33
+ # @param data [String] The serialized data.
34
+ # @param raise_errors [Boolean] Whether to raise decoding errors.
35
+ # @param default [Object] A fallback value to return when not raising errors.
36
+ # @return [Object] A deserialized value.
37
+ def self.load(data, raise_errors = false, default = {})
38
+ data = ActiveSupport::JSON.decode(data)
39
+ data = data.with_indifferent_access if data.is_a?(Hash)
40
+ data
41
+ rescue => e
42
+ raise(e) if raise_errors
43
+ default
44
+ end
45
+
46
+ # Saves serialized data.
47
+ #
48
+ # @param data [Object] The data to serialize.
49
+ # @return [String] Serialized data.
50
+ def self.dump(data)
51
+ ActiveSupport::JSON.encode(data.as_json)
52
+ end
53
+ end
54
+
55
+ # JWT encoded serialized value.
56
+ class JWT
57
+ class << self
58
+ # Loads serialized data.
59
+ #
60
+ # @param serialized [String] The serialized data.
61
+ # @param raise_errors [Boolean] Whether to raise decoding errors.
62
+ # @param default [Object] A fallback value to return when not raising errors.
63
+ # @return [Object] A deserialized value.
64
+ def load(serialized, raise_errors = false, default = {})
65
+ data = ::JWT.decode(serialized, jwt_secret, true, {algorithm: "HS256", verify_aud: true, aud: "data"}).dig(0, "sub")
66
+ data = data.with_indifferent_access if data.is_a?(Hash)
67
+ data
68
+ rescue => e
69
+ raise(e) if raise_errors
70
+ default
71
+ end
72
+
73
+ # Saves serialized data.
74
+ #
75
+ # @param data [Object] The data to serialize.
76
+ # @return [String] Serialized data.
77
+ def dump(data)
78
+ ::JWT.encode({aud: "data", sub: data.as_json}, jwt_secret, "HS256")
79
+ end
80
+
81
+ #:nodoc:
82
+ def jwt_secret
83
+ Apes::RuntimeConfiguration.jwt_token
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,233 @@
1
+ #
2
+ # This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
3
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
4
+ #
5
+
6
+ module Apes
7
+ # Utility class to parse URLs, domains and emails.
8
+ class UrlsParser
9
+ # The list of valid top level domains for a URL, email and domain.
10
+ # To update the list: http:jecas.cz/tld-list/
11
+ TLDS = [
12
+ "ac", "academy", "accountants", "actor", "ad", "ae", "aero", "af", "ag", "agency", "ai", "airforce", "al", "am", "an", "ao", "aq", "ar", "archi", "arpa",
13
+ "as", "asia", "associates", "at", "attorney", "au", "audio", "autos", "aw", "ax", "axa", "az",
14
+ "ba", "bar", "bargains", "bayern", "bb", "bd", "be", "beer", "berlin", "best", "bf", "bg", "bh", "bi", "bid", "bike", "bio", "biz", "bj", "black",
15
+ "blackfriday", "blue", "bm", "bn", "bo", "boutique", "br", "bs", "bt", "build", "builders", "buzz", "bv", "bw", "by", "bz",
16
+ "ca", "cab", "camera", "camp", "capital", "cards", "care", "career", "careers", "cash", "cat", "catering", "cc", "cd", "center", "ceo", "cf", "cg", "ch",
17
+ "cheap", "christmas", "church", "ci", "citic", "ck", "cl", "claims", "cleaning", "clinic", "clothing", "club", "cm", "cn", "co", "codes", "coffee",
18
+ "college", "cologne", "com", "community", "company", "computer", "condos", "construction", "consulting", "contractors", "cooking", "cool", "coop",
19
+ "country", "cr", "credit", "creditcard", "cruises", "cu", "cv", "cw", "cx", "cy", "cz",
20
+ "dance", "dating", "de", "dev", "degree", "democrat", "dental", "dentist", "desi", "diamonds", "digital",
21
+ "directory", "discount", "dj", "dk", "dm", "dnp", "do", "domains", "dz",
22
+ "ec", "edu", "education", "ee", "eg", "email", "engineering", "enterprises", "equipment", "er", "es", "estate", "et", "eu", "eus", "events", "exchange",
23
+ "expert", "exposed",
24
+ "fail", "farm", "feedback", "fi", "finance", "financial", "fish", "fishing", "fitness", "fj", "fk", "flights", "florist", "fm", "fo", "foo", "foundation",
25
+ "fr", "frogans", "fund", "furniture", "futbol",
26
+ "ga", "gal", "gallery", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gift", "gl", "glass", "globo", "gm", "gmo", "gn", "gop", "gov", "gp", "gq", "gr",
27
+ "graphics", "gratis", "gripe", "gs", "gt", "gu", "guide", "guitars", "guru", "gw", "gy",
28
+ "haus", "hiphop", "hiv", "hk", "hm", "hn", "holdings", "holiday", "homes", "horse", "host", "house", "hr", "ht", "hu",
29
+ "id", "ie", "il", "im", "immobilien", "in", "industries", "info", "ink", "institute", "insure", "int", "international", "investments", "io", "iq", "ir",
30
+ "is", "it",
31
+ "je", "jetzt", "jm", "jo", "jobs", "jp", "juegos",
32
+ "kaufen", "ke", "kg", "kh", "ki", "kim", "kitchen", "kiwi", "km", "kn", "koeln", "kp", "kr", "kred", "kw", "ky", "kz",
33
+ "la", "land", "lawyer", "lb", "lc", "lease", "li", "life", "lighting", "limited", "limo", "link", "lk", "loans", "london", "lr", "ls", "lt", "lu", "luxe",
34
+ "luxury", "lv", "ly",
35
+ "ma", "maison", "management", "mango", "market", "marketing", "mc", "md", "me", "media", "meet", "menu", "mg", "mh", "miami", "mil", "mk", "ml", "mm",
36
+ "mn", "mo", "mobi", "moda", "moe", "monash", "mortgage", "moscow", "motorcycles", "mp", "mq", "mr", "ms", "mt", "mu", "museum", "mv", "mw", "mx", "my",
37
+ "mz", "na", "nagoya", "name", "nc", "ne", "net", "neustar", "nf", "ng", "ni", "ninja", "nl", "no", "np", "nr", "nu", "nyc", "nz",
38
+ "okinawa", "om", "onl", "org",
39
+ "pa", "paris", "partners", "parts", "pe", "pf", "pg", "ph", "photo", "photography", "photos", "pics", "pictures", "pink", "pk", "pl", "plumbing", "pm",
40
+ "pn", "post", "pr", "press", "pro", "productions", "properties", "ps", "pt", "pub", "pw", "py",
41
+ "qa", "qpon", "quebec",
42
+ "re", "recipes", "red", "reise", "reisen", "ren", "rentals", "repair", "report", "rest", "reviews", "rich", "rio", "ro", "rocks", "rodeo", "rs", "ru",
43
+ "ruhr", "rw", "ryukyu",
44
+ "sa", "saarland", "sb", "sc", "schule", "sd", "se", "services", "sexy", "sg", "sh", "shiksha", "shoes", "si", "singles", "sj", "sk", "sl", "sm", "sn",
45
+ "so", "social", "software", "sohu", "solar", "solutions", "soy", "space", "sr", "st", "su", "supplies", "supply", "support", "surgery", "sv", "sx", "sy",
46
+ "systems", "sz",
47
+ "tattoo", "tax", "tc", "td", "technology", "tel", "tf", "tg", "th", "tienda", "tips", "tj", "tk", "tl", "tm", "tn", "to", "today", "tokyo", "tools",
48
+ "town", "toys", "tp", "tr", "trade", "training", "travel", "tt", "tv", "tw", "tz",
49
+ "ua", "ug", "uk", "university", "uno", "us", "uy", "uz",
50
+ "va", "vacations", "vc", "ve", "vegas", "ventures", "versicherung", "vet", "vg", "vi", "viajes", "villas", "vision", "vn", "vodka", "vote", "voting",
51
+ "voto", "voyage", "vu",
52
+ "wang", "watch", "webcam", "website", "wed", "wf", "wien", "wiki", "works", "ws", "wtc", "wtf",
53
+ "xn--3bst00m", "xn--3ds443g", "xn--3e0b707e", "xn--45brj9c", "xn--4gbrim", "xn--55qw42g", "xn--55qx5d", "xn--6frz82g", "xn--6qq986b3xl", "xn--80adxhks",
54
+ "xn--80ao21a", "xn--80asehdb", "xn--80aswg", "xn--90a3ac", "xn--c1avg", "xn--cg4bki", "xn--clchc0ea0b2g2a9gcd", "xn--czr694b", "xn--czru2d",
55
+ "xn--d1acj3b", "xn--fiq228c5hs", "xn--fiq64b", "xn--fiqs8s", "xn--fiqz9s", "xn--fpcrj9c3d", "xn--fzc2c9e2c", "xn--gecrj9c", "xn--h2brj9c",
56
+ "xn--i1b6b1a6a2e", "xn--io0a7i", "xn--j1amh", "xn--j6w193g", "xn--kprw13d", "xn--kpry57d", "xn--l1acc", "xn--lgbbat1ad8j", "xn--mgb9awbf",
57
+ "xn--mgba3a4f16a", "xn--mgbaam7a8h", "xn--mgbab2bd", "xn--mgbayh7gpa", "xn--mgbbh1a71e", "xn--mgbc0a9azcg", "xn--mgberp4a5d4ar", "xn--mgbx4cd0ab",
58
+ "xn--ngbc5azd", "xn--nqv7f", "xn--nqv7fs00ema", "xn--o3cw4h", "xn--ogbpf8fl", "xn--p1ai", "xn--pgbs0dh", "xn--q9jyb4c", "xn--rhqv96g", "xn--s9brj9c",
59
+ "xn--ses554g", "xn--unup4y", "xn--wgbh1c", "xn--wgbl6a", "xn--xkc2al3hye2a", "xn--xkc2dl3a5ee0h", "xn--yfro4i67o", "xn--ygbi2ammx", "xn--zfr164b", "xxx",
60
+ "xyz",
61
+ "yachts", "ye", "yokohama", "yt",
62
+ "za", "zm", "zone", "zw"
63
+ ].freeze
64
+
65
+ # Regular expression to match a valid URL.
66
+ URL_MATCHER = /
67
+ (
68
+ (http(s?):\/\/)? #PROTOCOL
69
+ (
70
+ (#{Resolv::IPv4::Regex.source.gsub("\\A", "").gsub("\\z", "")}) # IPv4
71
+ |
72
+ (\[(#{Resolv::IPv6::Regex.source.gsub("\\A", "").gsub("\\z", "")})\]) # IPv6
73
+ |
74
+ (
75
+ (([\w\-\_]+\.)*) # LOWEST TLD
76
+ ([\w\-\_]+) # 2nd LEVEL TLD
77
+ (\.(#{TLDS.join("|")})) # TOP TLD
78
+ )
79
+ )
80
+ ((:\d+)?) # PORT
81
+ ((\/\S*)?) # PATH
82
+ ((\?\S+)?) # QUERY
83
+ ((\#\S*)?) # FRAGMENT
84
+ )
85
+ /ix
86
+
87
+ # Regular expression to detect a URL in a text.
88
+ URL_SEPARATOR = /[\[\]!"#$%&'()*+,.:;<=>?~\s@\^_`\{|\}\-]/
89
+
90
+ # Regular expression to match a valid email address.
91
+ EMAIL_MATCHER = /
92
+ ^(
93
+ ([\w\+\-\_\.\']+@) #HOST
94
+ (([\w\-]+\.)*) # LOWEST TLD
95
+ ([\w\-_]+) # 2nd LEVEL TLD
96
+ (\.(#{TLDS.join("|")})) # TOP TLD
97
+ )$
98
+ /ix
99
+
100
+ # Regular expression to match a valid domain.
101
+ DOMAIN_MATCHER = /
102
+ ^(
103
+ (([\w\-]+\.)*) # LOWEST TLD
104
+ ([\w\-_]+) # 2nd LEVEL TLD
105
+ (\.(#{TLDS.join("|")})) # TOP TLD
106
+ )$
107
+ /ix
108
+
109
+ # Template to replace URLs in a text.
110
+ TEMPLATE = "{{urls.url_%s}}".freeze
111
+
112
+ # Get the singleton instance of the parser.
113
+ #
114
+ # @param force [Boolean] Whether to force creation of a new singleton.
115
+ # @return [Apes::UrlsParser] A instance of the parser.
116
+ def self.instance(force = false)
117
+ @instance = nil if force
118
+ @instance ||= new
119
+ end
120
+
121
+ # Checks if the value is a valid URL.
122
+ #
123
+ # @return [Boolean] `true` if the value is a valid URL, `false` otherwise.
124
+ def url?(url)
125
+ url.strip =~ /^(#{UrlsParser::URL_MATCHER.source})$/ix ? true : false
126
+ end
127
+
128
+ # Checks if the value is a valid email address.
129
+ #
130
+ # @return [Boolean] `true` if the value is a valid email address, `false` otherwise.
131
+ def email?(email)
132
+ email.strip =~ /^(#{UrlsParser::EMAIL_MATCHER.source})$/ix ? true : false
133
+ end
134
+
135
+ # Checks if the value is a valid domain.
136
+ #
137
+ # @return [Boolean] `true` if the value is a valid domain, `false` otherwise.
138
+ def domain?(domain)
139
+ domain.strip =~ /^(#{UrlsParser::DOMAIN_MATCHER.source})$/ix ? true : false
140
+ end
141
+
142
+ # Checks if the value is a shortened URL according to the provided shortened domains.
143
+ #
144
+ # @return [Boolean] `true` if the value is a shortend URL, `false` otherwise.
145
+ def shortened?(url, *shortened_domains)
146
+ domains = ["bit.ly"].concat(shortened_domains).uniq.compact.map(&:strip)
147
+ url?(url) && (ensure_url_with_scheme(url.strip) =~ /^(http(s?):\/\/(#{domains.map { |d| Regexp.quote(d) }.join("|")}))/i ? true : false)
148
+ end
149
+
150
+ # Makes sure the string starts with the scheme for the specified protocol.
151
+ #
152
+ # @param subject [String] The string to analyze.
153
+ # @param protocol [String] The protocol for the URL.
154
+ # @param secure [Boolean] If the scheme should be secure or not.
155
+ # @return [String] The string with a URL scheme at the beginning.
156
+ def ensure_url_with_scheme(subject, protocol = "http", secure: false)
157
+ schema = protocol + (secure ? "s" : "")
158
+ subject !~ /^(#{protocol}(s?):\/\/)/ ? "#{schema}://#{subject}" : subject
159
+ end
160
+
161
+ # Extract all URLS from a text.
162
+ #
163
+ # @param text [String] The text that contains URLs.
164
+ # @param mode [Symbol] Which URLs to extract. It can be `:shortened`, `:unshortened` or `:all` (the default).
165
+ # @param sort [NilClass|Symbol] If not `nil`, how to sort extracted URLs. It can be `:asc` or `:desc`.
166
+ # @param shortened_domains [Array] Which domains to consider shortened.
167
+ # @return [Array] An array of extracted URLs.
168
+ def extract_urls(text, mode: :all, sort: nil, shortened_domains: [])
169
+ regexp = /((^|\s+)(?<url>#{UrlsParser::URL_MATCHER.source})(#{UrlsParser::URL_SEPARATOR.source}|$))/ix
170
+ matches = text.scan(regexp).flatten.map { |u| clean(u) }.uniq
171
+
172
+ if mode == :shortened
173
+ matches.select! { |u| shortened?(u, *shortened_domains) }
174
+ elsif mode == :unshortened
175
+ matches.reject! { |u| shortened?(u, *shortened_domains) }
176
+ end
177
+
178
+ matches = sort_urls(matches, sort)
179
+ matches
180
+ end
181
+
182
+ # Replace all URLs in a text with provided replacements.
183
+ #
184
+ # @param text [String] The text that contains URLs.
185
+ # @param replacements [Hash] A map where keys are the URLs to replace and values are their replacements.
186
+ # @param mode [Symbol] Which URLs to extract. It can be `:shortened`, `:unshortened` or `:all` (the default).
187
+ # @param shortened_domains [Array] Which domains to consider shortened.
188
+ # @return [String] The original text with all URLs replaced.
189
+ def replace_urls(text, replacements: {}, mode: :all, shortened_domains: [])
190
+ text = text.dup
191
+
192
+ urls = extract_urls(text, mode: mode, sort: :desc, shortened_domains: shortened_domains).reduce({}) do |accu, url|
193
+ if replacements[url]
194
+ hash = hashify(url)
195
+ accu["url_#{hash}"] = ensure_url_with_scheme(replacements[url])
196
+ text.gsub!(/#{Regexp.quote(url)}/, format(UrlsParser::TEMPLATE, hash))
197
+ end
198
+
199
+ accu
200
+ end
201
+
202
+ Mustache.render(text, urls: urls)
203
+ end
204
+
205
+ # Removes all extra characters (like trailing comma) from a URL.
206
+ #
207
+ # @param url [String] The URL to clean.
208
+ # @return [String] The cleaned URL.
209
+ def clean(url)
210
+ url.strip.gsub(/#{UrlsParser::URL_SEPARATOR.source}$/, "")
211
+ end
212
+
213
+ # Generate a hash of a URL.
214
+ #
215
+ # @param url [String] The URL to hashify.
216
+ # @return [String] The hash for the URL.
217
+ def hashify(url)
218
+ Digest::SHA2.hexdigest(ensure_url_with_scheme(url.strip))
219
+ end
220
+
221
+ private
222
+
223
+ # :nodoc:
224
+ def sort_urls(matches, sort)
225
+ if sort
226
+ matches.sort! { |u1, u2| u1.length <=> u2.length }
227
+ matches.reverse! if sort == :desc
228
+ end
229
+
230
+ matches
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,234 @@
1
+ #
2
+ # This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
3
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
4
+ #
5
+
6
+ module Apes
7
+ # A useful set of validators.
8
+ module Validators
9
+ # The base validator.
10
+ class BaseValidator < ActiveModel::EachValidator
11
+ # Perform validation on a attribute of a model.
12
+ #
13
+ # @param model [Object] The object to validate.
14
+ # @param attribute [String|Symbol] The attribute to validate.
15
+ # @param value [Object] The value of the attribute.
16
+ def validate_each(model, attribute, value)
17
+ checked = check_valid?(value)
18
+ return checked if checked
19
+
20
+ message = options[:message] || options[:default_message]
21
+ destination = options[:additional] ? model.additional_errors : model.errors
22
+ destination[attribute] << message
23
+ nil
24
+ end
25
+ end
26
+
27
+ # Validates references (relationships in the JSON API nomenclature).
28
+ class ReferenceValidator < BaseValidator
29
+ # Creates a new validator.
30
+ #
31
+ # @param options [Hash] The options for the validations.
32
+ # @return [Apes::Validators::ReferenceValidator] A new validator.
33
+ def initialize(options)
34
+ @class_name = options[:class_name]
35
+ label = options[:label] || options[:class_name].classify
36
+ super(options.reverse_merge(default_message: "must be a valid #{label} (cannot find a #{label} with id \"%s\")"))
37
+ end
38
+
39
+ # Perform validation on a attribute of a model.
40
+ #
41
+ # @param model [Object] The object to validate.
42
+ # @param attribute [String|Symbol] The attribute to validate.
43
+ # @param values [Array] The values of the attribute.
44
+ def validate_each(model, attribute, values)
45
+ values = Serializers::JSON.load(values, false, values)
46
+
47
+ values.ensure_array.each do |value|
48
+ checked = @class_name.classify.constantize.find_with_any(value)
49
+ add_failure(attribute, model, value) unless checked
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # :nodoc:
56
+ def add_failure(attribute, record, value)
57
+ message = options[:message] || options[:default_message]
58
+ destination = options[:additional] ? record.additional_errors : record.errors
59
+ destination[attribute] << sprintf(message, value)
60
+ end
61
+ end
62
+
63
+ # Validates UUIDs (version 4).
64
+ class UuidValidator < BaseValidator
65
+ # The pattern to recognized a valid UUID version 4.
66
+ VALID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
67
+
68
+ # Creates a new validator.
69
+ #
70
+ # @param options [Hash] The options for the validations.
71
+ # @return [Apes::Validators::UuidValidator] A new validator.
72
+ def initialize(options)
73
+ super(options.reverse_merge(default_message: "must be a valid UUID"))
74
+ end
75
+
76
+ # Checks if the value is valid for this validator.
77
+ #
78
+ # @param value [Object] The value to validate.
79
+ # @return [Boolean] `true` if the value is valid, false otherwise.
80
+ def check_valid?(value)
81
+ value.blank? || value =~ VALID_REGEX
82
+ end
83
+ end
84
+
85
+ # Validates email.
86
+ class EmailValidator < BaseValidator
87
+ # Creates a new validator.
88
+ #
89
+ # @param options [Hash] The options for the validations.
90
+ # @return [Apes::Validators::EmailValidator] A new validator.
91
+ def initialize(options)
92
+ super(options.reverse_merge(default_message: "must be a valid email"))
93
+ end
94
+
95
+ # Checks if the value is valid for this validator.
96
+ #
97
+ # @param value [Object] The value to validate.
98
+ # @return [Boolean] `true` if the value is valid, false otherwise.
99
+ def check_valid?(value)
100
+ value.blank? || UrlsParser.instance.email?(value.ensure_string)
101
+ end
102
+ end
103
+
104
+ # Validates boolean values.
105
+ class BooleanValidator < BaseValidator
106
+ # Parses a boolean value.
107
+ #
108
+ # @param value [Object] The value to parse.
109
+ # @param raise_errors [Boolean] Whether to raise errors in case the value couldn't be parsed.
110
+ # @return [Boolean|NilClass] A boolean value if parsing succeded, `nil` otherwise.
111
+ def self.parse(value, raise_errors: false)
112
+ raise(ArgumentError, "Invalid boolean value \"#{value}\".") if !value.nil? && !value.boolean? && raise_errors
113
+ value.to_boolean
114
+ end
115
+
116
+ # Creates a new validator.
117
+ #
118
+ # @param options [Hash] The options for the validations.
119
+ # @return [Apes::Validators::BooleanValidator] A new validator.
120
+ def initialize(options)
121
+ super(options.reverse_merge(default_message: "must be a valid truthy/falsey value"))
122
+ end
123
+
124
+ # Checks if the value is valid for this validator.
125
+ #
126
+ # @param value [Object] The value to validate.
127
+ # @return [Boolean] `true` if the value is valid, false otherwise.
128
+ def check_valid?(value)
129
+ value.blank? || value.boolean?
130
+ end
131
+ end
132
+
133
+ # Validates phones.
134
+ class PhoneValidator < BaseValidator
135
+ # The pattern to recognize valid phones.
136
+ VALID_REGEX = /^(
137
+ ((\+|00)\d)? # International prefix
138
+ ([0-9\-\s\/\(\)]{7,}) # All the rest
139
+ )$/mx
140
+
141
+ # Creates a new validator.
142
+ #
143
+ # @param options [Hash] The options for the validations.
144
+ # @return [Apes::Validators::PhoneValidator] A new validator.
145
+ def initialize(options)
146
+ super(options.reverse_merge(default_message: "must be a valid phone"))
147
+ end
148
+
149
+ # Checks if the value is valid for this validator.
150
+ #
151
+ # @param value [Object] The value to validate.
152
+ # @return [Boolean] `true` if the value is valid, false otherwise.
153
+ def check_valid?(value)
154
+ value.blank? || value =~ VALID_REGEX
155
+ end
156
+ end
157
+
158
+ # Validates ZIP codes.
159
+ class ZipCodeValidator < BaseValidator
160
+ # The pattern to recognized valid ZIP codes.
161
+ VALID_REGEX = /^(\d{5}(-\d{1,4})?)$/
162
+
163
+ # Creates a new validator.
164
+ #
165
+ # @param options [Hash] The options for the validations.
166
+ # @return [Apes::Validators::ZipCodeValidator] A new validator.
167
+ def initialize(options)
168
+ super(options.reverse_merge(default_message: "must be a valid ZIP code"))
169
+ end
170
+
171
+ # Checks if the value is valid for this validator.
172
+ #
173
+ # @param value [Object] The value to validate.
174
+ # @return [Boolean] `true` if the value is valid, false otherwise.
175
+ def check_valid?(value)
176
+ value.blank? || value =~ VALID_REGEX
177
+ end
178
+ end
179
+
180
+ # Validates timestamps.
181
+ class TimestampValidator < BaseValidator
182
+ # Parses a timestamp value according to a list of formats.
183
+ #
184
+ # @param value [Object] The value to parse.
185
+ # @param formats [Array|NilClass] A list of valid formats (see strftime). Will fallback to formats defined in Rails configuration.
186
+ # @param raise_errors [Boolean] Whether to raise errors in case the value couldn't be parsed with any format.
187
+ # @return [DateTime|NilClass] A `DateTime` if parsing succeded, `nil` otherwise.
188
+ def self.parse(value, formats: nil, raise_errors: false)
189
+ return value if [ActiveSupport::TimeWithZone, DateTime, Date, Time].include?(value.class)
190
+
191
+ formats ||= Apes::RuntimeConfiguration.timestamp_formats.values.dup
192
+
193
+ rv = catch(:valid) do
194
+ formats.each do |format|
195
+ parsed = safe_parse(value, format)
196
+
197
+ throw(:valid, parsed) if parsed
198
+ end
199
+
200
+ nil
201
+ end
202
+
203
+ raise(ArgumentError, "Invalid timestamp \"#{value}\".") if !rv && raise_errors
204
+ rv
205
+ end
206
+
207
+ # Parses a timestamp without raising exceptions.
208
+ #
209
+ # @param value [String] The value to parse.
210
+ # @return [DateTime|NilClass] A `DateTime` if parsing succeded, `nil` otherwise.
211
+ def self.safe_parse(value, format)
212
+ DateTime.strptime(value, format)
213
+ rescue
214
+ nil
215
+ end
216
+
217
+ # Creates a new validator.
218
+ #
219
+ # @param options [Hash] The options for the validations.
220
+ # @return [Apes::Validators::TimestampValidator] A new validator.
221
+ def initialize(options)
222
+ super(options.reverse_merge(default_message: "must be a valid ISO 8601 timestamp"))
223
+ end
224
+
225
+ # Checks if the value is valid for this validator.
226
+ #
227
+ # @param value [Object] The value to validate.
228
+ # @return [Boolean] `true` if the value is valid, false otherwise.
229
+ def check_valid?(value)
230
+ value.blank? || TimestampValidator.parse(value, formats: options[:formats])
231
+ end
232
+ end
233
+ end
234
+ end