stringex 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +16 -0
  3. data/Gemfile.lock +74 -0
  4. data/README.rdoc +22 -1
  5. data/Rakefile +46 -223
  6. data/VERSION +1 -0
  7. data/init.rb +1 -0
  8. data/lib/stringex.rb +11 -3
  9. data/lib/stringex/acts_as_url.rb +49 -97
  10. data/lib/stringex/acts_as_url/adapter.rb +26 -0
  11. data/lib/stringex/acts_as_url/adapter/active_record.rb +23 -0
  12. data/lib/stringex/acts_as_url/adapter/base.rb +188 -0
  13. data/lib/stringex/acts_as_url/adapter/data_mapper.rb +67 -0
  14. data/lib/stringex/acts_as_url/adapter/mongoid.rb +36 -0
  15. data/lib/stringex/configuration.rb +4 -0
  16. data/lib/stringex/configuration/acts_as_url.rb +44 -0
  17. data/lib/stringex/configuration/base.rb +58 -0
  18. data/lib/stringex/configuration/configurator.rb +25 -0
  19. data/lib/stringex/configuration/string_extensions.rb +19 -0
  20. data/lib/stringex/localization.rb +98 -0
  21. data/lib/stringex/localization/backend/i18n.rb +53 -0
  22. data/lib/stringex/localization/backend/internal.rb +51 -0
  23. data/lib/stringex/localization/conversion_expressions.rb +148 -0
  24. data/lib/stringex/localization/converter.rb +121 -0
  25. data/lib/stringex/localization/default_conversions.rb +88 -0
  26. data/lib/stringex/rails/railtie.rb +10 -0
  27. data/lib/stringex/string_extensions.rb +153 -208
  28. data/lib/stringex/unidecoder.rb +6 -101
  29. data/lib/stringex/unidecoder_data/x00.yml +1 -1
  30. data/lib/stringex/unidecoder_data/x02.yml +5 -5
  31. data/lib/stringex/unidecoder_data/x05.yml +1 -1
  32. data/lib/stringex/unidecoder_data/x06.yml +1 -1
  33. data/lib/stringex/unidecoder_data/x07.yml +3 -3
  34. data/lib/stringex/unidecoder_data/x09.yml +1 -1
  35. data/lib/stringex/unidecoder_data/x0e.yml +2 -2
  36. data/lib/stringex/unidecoder_data/x1f.yml +2 -2
  37. data/lib/stringex/unidecoder_data/x20.yml +1 -1
  38. data/lib/stringex/unidecoder_data/xfb.yml +1 -1
  39. data/lib/stringex/unidecoder_data/xff.yml +1 -1
  40. data/lib/stringex/version.rb +8 -0
  41. data/locales/da.yml +73 -0
  42. data/locales/en.yml +66 -0
  43. data/stringex.gemspec +77 -18
  44. data/test/acts_as_url/adapter/active_record.rb +72 -0
  45. data/test/acts_as_url/adapter/data_mapper.rb +82 -0
  46. data/test/acts_as_url/adapter/mongoid.rb +73 -0
  47. data/test/acts_as_url_configuration_test.rb +51 -0
  48. data/test/acts_as_url_integration_test.rb +271 -0
  49. data/test/localization/da_test.rb +117 -0
  50. data/test/localization/default_test.rb +113 -0
  51. data/test/localization/en_test.rb +117 -0
  52. data/test/localization_test.rb +123 -0
  53. data/test/redcloth_to_html_test.rb +37 -0
  54. data/test/string_extensions_test.rb +59 -91
  55. data/test/test_helper.rb +2 -0
  56. data/test/unicode_point_suite/basic_greek_test.rb +113 -0
  57. data/test/unicode_point_suite/basic_latin_test.rb +142 -0
  58. data/test/unicode_point_suite/codepoint_test_helper.rb +32 -0
  59. data/test/unidecoder/bad_localization.yml +1 -0
  60. data/test/unidecoder/localization.yml +4 -0
  61. data/test/unidecoder_test.rb +3 -5
  62. metadata +145 -37
  63. data/test/acts_as_url_test.rb +0 -272
data/lib/stringex.rb CHANGED
@@ -1,9 +1,17 @@
1
1
  # encoding: UTF-8
2
+
3
+ require 'ostruct'
4
+ require 'stringex/configuration'
5
+ require 'stringex/localization'
2
6
  require 'stringex/string_extensions'
3
7
  require 'stringex/unidecoder'
8
+ require 'stringex/acts_as_url'
4
9
 
5
- String.send :include, Stringex::StringExtensions
10
+ String.send :include, Stringex::StringExtensions::PublicInstanceMethods
11
+ String.send :extend, Stringex::StringExtensions::PublicClassMethods
6
12
 
7
- require 'stringex/acts_as_url' if defined?(ActiveRecord) || defined?(Mongoid::Document)
13
+ Stringex::ActsAsUrl::Adapter.load_available
8
14
 
9
- ActiveRecord::Base.send :include, Stringex::ActsAsUrl if defined?(ActiveRecord)
15
+ if defined?(Rails::Railtie)
16
+ require 'stringex/rails/railtie'
17
+ end
@@ -1,66 +1,17 @@
1
1
  # encoding: UTF-8
2
+ require "stringex/acts_as_url/adapter"
3
+
2
4
  module Stringex
3
5
  module ActsAsUrl # :nodoc:
4
- def self.included(base)
5
- base.extend ClassMethods
6
+ def self.configure(&block)
7
+ Stringex::Configuration::ActsAsUrl.configure &block
6
8
  end
7
9
 
8
- class Configuration
9
- attr_accessor :allow_slash, :allow_duplicates, :attribute_to_urlify, :duplicate_count_separator,
10
- :exclude, :only_when_blank, :scope_for_url, :sync_url, :url_attribute, :url_limit
11
-
12
- def initialize(klass, options = {})
13
- self.allow_slash = options[:allow_slash]
14
- self.allow_duplicates = options[:allow_duplicates]
15
- self.attribute_to_urlify = options[:attribute]
16
- self.duplicate_count_separator = options[:duplicate_count_separator] || "-"
17
- self.exclude = options[:exclude] || []
18
- self.only_when_blank = options[:only_when_blank]
19
- self.scope_for_url = options[:scope]
20
- self.sync_url = options[:sync_url]
21
- self.url_attribute = options[:url_attribute] || "url"
22
- self.url_limit = options[:limit]
23
- end
24
-
25
- def get_base_url!(instance)
26
- base_url = instance.send(url_attribute)
27
- if base_url.blank? || !only_when_blank
28
- root = instance.send(attribute_to_urlify).to_s
29
- base_url = root.to_url(:allow_slash => allow_slash, :limit => url_limit, :exclude => exclude)
30
- end
31
- instance.instance_variable_set "@acts_as_url_base_url", base_url
32
- end
33
-
34
- def get_conditions!(instance)
35
- conditions = ["#{url_attribute} LIKE ?", instance.instance_variable_get("@acts_as_url_base_url") + '%']
36
- unless instance.new_record?
37
- conditions.first << " and id != ?"
38
- conditions << instance.id
39
- end
40
- if scope_for_url
41
- conditions.first << " and #{scope_for_url} = ?"
42
- conditions << instance.send(scope_for_url)
43
- end
44
- conditions
45
- end
46
-
47
- def handle_duplicate_urls!(instance)
48
- return if allow_duplicates
49
-
50
- base_url = instance.instance_variable_get("@acts_as_url_base_url")
51
- url_owners = instance.class.unscoped.find(:all, :conditions => get_conditions!(instance))
52
- if url_owners.any?{|owner| owner.send(url_attribute) == base_url}
53
- separator = duplicate_count_separator
54
- n = 1
55
- while url_owners.any?{|owner| owner.send(url_attribute) == "#{base_url}#{separator}#{n}"}
56
- n = n.succ
57
- end
58
- instance.send :write_attribute, url_attribute, "#{base_url}#{separator}#{n}"
59
- end
60
- end
10
+ def self.unconfigure!
11
+ Stringex::Configuration::ActsAsUrl.unconfigure!
61
12
  end
62
13
 
63
- module ClassMethods # :doc:
14
+ module ActsAsUrlClassMethods # :doc:
64
15
  # Creates a callback to automatically create an url-friendly representation
65
16
  # of the <tt>attribute</tt> argument. Example:
66
17
  #
@@ -74,48 +25,58 @@ module Stringex
74
25
  # The default attribute <tt>acts_as_url</tt> uses to save the permalink is <tt>url</tt>
75
26
  # but this can be changed in the options hash. Available options are:
76
27
  #
77
- # <tt>:allow_slash</tt>:: If true, allow the generated url to contain slashes. Default is false[y].
78
- # <tt>:allow_duplicates</tt>:: If true, allow duplicate urls instead of appending numbers to
79
- # differentiate between urls. Default is false[y].
28
+ # <tt>:adapter</tt>:: If specified, will indicate what ORM adapter to use. Default functionality
29
+ # is to use the first available adapter. This should work for most cases
30
+ # unless you are using multiple ORMs in a single project.
31
+ # <tt>:allow_slash</tt>:: If true, allows the generated url to contain slashes. Default is false[y].
32
+ # <tt>:allow_duplicates</tt>:: If true, allows duplicate urls instead of appending numbers to
33
+ # differentiate between urls. Default is false[y]. See note on <tt>:scope</tt>.
80
34
  # <tt>:duplicate_count_separator</tt>:: String to use when forcing unique urls from non-unique strings.
81
35
  # Default is "-".
36
+ # <tt>:force_downcase</tt>:: If false, allows generated url to contain uppercased letters. Default is false.
82
37
  # <tt>:exclude_list</tt>:: List of complete strings that should not be transformed by <tt>acts_as_url</tt>.
83
38
  # Default is empty.
84
39
  # <tt>:only_when_blank</tt>:: If true, the url generation will only happen when <tt>:url_attribute</tt> is
85
40
  # blank. Default is false[y] (meaning url generation will happen always).
86
41
  # <tt>:scope</tt>:: The name of model attribute to scope unique urls to. There is no default here.
42
+ # <strong>Note:</strong> this will automatically act as if <tt>:allow_duplicates</tt>
43
+ # is set to true.
87
44
  # <tt>:sync_url</tt>:: If set to true, the url field will be updated when changes are made to the
88
- # attribute it is based on. Default is false[y].
45
+ # attribute it is based on. Default is false.
89
46
  # <tt>:url_attribute</tt>:: The name of the attribute to use for storing the generated url string.
90
47
  # Default is <tt>:url</tt>.
91
- # <tt>:url_limit</tt>:: The maximum size a generated url should be. <strong>Note:</strong> this does not
92
- # include the characters needed to enforce uniqueness on duplicate urls.
93
- # Default is nil.
48
+ # <tt>:limit</tt>:: The maximum size a generated url should be. <strong>Note:</strong> this does not
49
+ # include the characters needed to enforce uniqueness on duplicate urls.
50
+ # Default is nil.
94
51
  def acts_as_url(attribute, options = {})
95
- cattr_accessor :acts_as_url_configuration
96
-
97
- options[:attribute] = attribute
98
- self.acts_as_url_configuration = ActsAsUrl::Configuration.new(self, options)
99
-
100
- if acts_as_url_configuration.sync_url
101
- before_validation(:ensure_unique_url)
102
- else
103
- if defined?(ActiveModel::Callbacks)
104
- before_validation(:ensure_unique_url, :on => :create)
105
- else
106
- before_validation_on_create(:ensure_unique_url)
52
+ class_eval do
53
+ class << self
54
+ attr_accessor :acts_as_url_configuration
107
55
  end
108
- end
109
56
 
110
- class_eval <<-"END"
111
- def #{acts_as_url_configuration.url_attribute}
112
- if !new_record? && errors[acts_as_url_configuration.attribute_to_urlify].present?
113
- self.class.find(id).send(acts_as_url_configuration.url_attribute)
114
- else
115
- read_attribute(acts_as_url_configuration.url_attribute)
57
+ define_method :acts_as_url_configuration do
58
+ klass = self.class
59
+ while klass.acts_as_url_configuration.nil?
60
+ klass = klass.superclass
116
61
  end
62
+ klass.acts_as_url_configuration
117
63
  end
118
- END
64
+ end
65
+
66
+ options[:attribute_to_urlify] = attribute
67
+ self.acts_as_url_configuration = Stringex::Configuration::ActsAsUrl.new(options)
68
+
69
+ acts_as_url_configuration.adapter.create_callbacks! self
70
+ end
71
+
72
+
73
+ # Some ORMs function as mixins not base classes and need to have a hook to reinclude
74
+ # and re-extend ActsAsUrl methods
75
+ def included(base)
76
+ super
77
+
78
+ base.send :include, Stringex::ActsAsUrl::ActsAsUrlInstanceMethods
79
+ base.send :extend, Stringex::ActsAsUrl::ActsAsUrlClassMethods
119
80
  end
120
81
 
121
82
  # Initialize the url fields for the records that need it. Designed for people who add
@@ -126,23 +87,14 @@ module Stringex
126
87
  # on a large selection, you will get much better results writing your own version with
127
88
  # using pagination.
128
89
  def initialize_urls
129
- find_each(:conditions => {acts_as_url_configuration.url_attribute => nil}) do |instance|
130
- instance.send :ensure_unique_url
131
- instance.save
132
- end
90
+ acts_as_url_configuration.adapter.initialize_urls! self
133
91
  end
134
92
  end
135
93
 
136
- private
137
-
138
- def ensure_unique_url
139
- # Just to save some typing
140
- config = acts_as_url_configuration
141
- url_attribute = config.url_attribute
142
-
143
- config.get_base_url! self
144
- write_attribute url_attribute, @acts_as_url_base_url
145
- config.handle_duplicate_urls!(self) unless config.allow_duplicates
94
+ module ActsAsUrlInstanceMethods
95
+ def ensure_unique_url
96
+ acts_as_url_configuration.adapter.ensure_unique_url! self
97
+ end
146
98
  end
147
99
  end
148
100
  end
@@ -0,0 +1,26 @@
1
+ require "stringex/acts_as_url/adapter/base"
2
+ require "stringex/acts_as_url/adapter/active_record"
3
+ require "stringex/acts_as_url/adapter/data_mapper"
4
+ require "stringex/acts_as_url/adapter/mongoid"
5
+
6
+ module Stringex
7
+ module ActsAsUrl
8
+ module Adapter
9
+ def self.add_loaded_adapter(adapter)
10
+ @loaded_adapters << adapter
11
+ end
12
+
13
+ def self.load_available
14
+ @loaded_adapters = []
15
+ constants.each do |name|
16
+ adapter = const_get(name)
17
+ adapter.load if adapter.loadable?
18
+ end
19
+ end
20
+
21
+ def self.first_available
22
+ @loaded_adapters[0]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ module Stringex
2
+ module ActsAsUrl
3
+ module Adapter
4
+ class ActiveRecord < Base
5
+ def self.load
6
+ ensure_loadable
7
+ orm_class.send :include, ActsAsUrlInstanceMethods
8
+ orm_class.send :extend, ActsAsUrlClassMethods
9
+ end
10
+
11
+ private
12
+
13
+ def klass_previous_instances(&block)
14
+ klass.find_each(:conditions => {settings.url_attribute => nil}, &block)
15
+ end
16
+
17
+ def self.orm_class
18
+ ::ActiveRecord::Base
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,188 @@
1
+ module Stringex
2
+ module ActsAsUrl
3
+ module Adapter
4
+ class Base
5
+ attr_accessor :base_url, :configuration, :instance, :klass, :settings
6
+
7
+ def initialize(configuration)
8
+ ensure_loadable
9
+ self.configuration = configuration
10
+ self.settings = configuration.settings
11
+ end
12
+
13
+ def create_callbacks!(klass)
14
+ self.klass = klass
15
+ create_method_to_callback
16
+ create_callback
17
+ end
18
+
19
+ def ensure_unique_url!(instance)
20
+ @url_owners = nil
21
+ self.instance = instance
22
+
23
+ handle_url!
24
+ handle_duplicate_url! unless settings.allow_duplicates
25
+ end
26
+
27
+ def initialize_urls!(klass)
28
+ self.klass = klass
29
+ klass_previous_instances do |instance|
30
+ ensure_unique_url_for! instance
31
+ end
32
+ end
33
+
34
+ def url_attribute(instance)
35
+ # Retrieve from database record if there are errors on attribute_to_urlify
36
+ if !is_new?(instance) && is_present?(instance.errors[settings.attribute_to_urlify])
37
+ self.instance = instance
38
+ read_attribute instance_from_db, settings.url_attribute
39
+ else
40
+ read_attribute instance, settings.url_attribute
41
+ end
42
+ end
43
+
44
+ def self.ensure_loadable
45
+ raise "The #{self} adapter cannot be loaded" unless loadable?
46
+ Stringex::ActsAsUrl::Adapter.add_loaded_adapter self
47
+ end
48
+
49
+ def self.loadable?
50
+ orm_class
51
+ rescue NameError
52
+ false
53
+ end
54
+
55
+ private
56
+
57
+ def add_new_record_url_owner_conditions
58
+ return if is_new?(instance)
59
+ @url_owner_conditions.first << " and #{instance.class.primary_key} != ?"
60
+ @url_owner_conditions << instance.id
61
+ end
62
+
63
+ def add_scoped_url_owner_conditions
64
+ return unless settings.scope_for_url
65
+ @url_owner_conditions.first << " and #{settings.scope_for_url} = ?"
66
+ @url_owner_conditions << instance.send(settings.scope_for_url)
67
+ end
68
+
69
+ def create_callback
70
+ if settings.sync_url
71
+ klass.before_validation :ensure_unique_url
72
+ else
73
+ klass.before_validation :ensure_unique_url, :on => :create
74
+ end
75
+ end
76
+
77
+ def create_method_to_callback
78
+ klass.class_eval <<-"END"
79
+ def #{settings.url_attribute}
80
+ acts_as_url_configuration.adapter.url_attribute self
81
+ end
82
+ END
83
+ end
84
+
85
+ def duplicate_for_base_url(n)
86
+ "#{base_url}#{settings.duplicate_count_separator}#{n}"
87
+ end
88
+
89
+ def ensure_loadable
90
+ self.class.ensure_loadable
91
+ end
92
+
93
+ # NOTE: The <tt>instance</tt> here is not the cached instance but a block variable
94
+ # passed from <tt>klass_previous_instances</tt>, just to be clear
95
+ def ensure_unique_url_for!(instance)
96
+ instance.send :ensure_unique_url
97
+ instance.save
98
+ end
99
+
100
+ def get_base_url_owner_conditions
101
+ @url_owner_conditions = ["#{settings.url_attribute} LIKE ?", base_url + '%']
102
+ end
103
+
104
+ def handle_duplicate_url!
105
+ return if url_owners.none?{|owner| url_attribute_for(owner) == base_url}
106
+ n = 1
107
+ while url_owners.any?{|owner| url_attribute_for(owner) == duplicate_for_base_url(n)}
108
+ n = n.succ
109
+ end
110
+ write_url_attribute duplicate_for_base_url(n)
111
+ end
112
+
113
+ def handle_url!
114
+ self.base_url = instance.send(settings.url_attribute)
115
+ modify_base_url if is_blank?(base_url) || !settings.only_when_blank
116
+ write_url_attribute base_url
117
+ end
118
+
119
+ def instance_from_db
120
+ instance.class.find(instance.id)
121
+ end
122
+
123
+ def is_blank?(object)
124
+ object.blank?
125
+ end
126
+
127
+ def is_new?(object)
128
+ object.new_record?
129
+ end
130
+
131
+ def is_present?(object)
132
+ object.present?
133
+ end
134
+
135
+ def loadable?
136
+ self.class.loadable?
137
+ end
138
+
139
+ def modify_base_url
140
+ root = instance.send(settings.attribute_to_urlify).to_s
141
+ self.base_url = root.to_url(configuration.string_extensions_settings)
142
+ end
143
+
144
+ def orm_class
145
+ self.class.orm_class
146
+ end
147
+
148
+ def read_attribute(instance, attribute)
149
+ instance.read_attribute attribute
150
+ end
151
+
152
+ def url_attribute_for(object)
153
+ object.send settings.url_attribute
154
+ end
155
+
156
+ def url_owner_conditions
157
+ get_base_url_owner_conditions
158
+ add_new_record_url_owner_conditions
159
+ add_scoped_url_owner_conditions
160
+
161
+ @url_owner_conditions
162
+ end
163
+
164
+ def url_owners
165
+ @url_owners ||= url_owners_class.unscoped.where(url_owner_conditions).to_a
166
+ end
167
+
168
+ def url_owners_class
169
+ return instance.class unless settings.enforce_uniqueness_on_sti_base_class
170
+
171
+ klass = instance.class
172
+ while klass.superclass < orm_class
173
+ klass = klass.superclass
174
+ end
175
+ klass
176
+ end
177
+
178
+ def write_attribute(instance, attribute, value)
179
+ instance.send :write_attribute, attribute, value
180
+ end
181
+
182
+ def write_url_attribute(value)
183
+ write_attribute instance, settings.url_attribute, value
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end