apes 1.0.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.
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