humanid 1.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: 1a64d1071c4dbf56186641557dfba393fdf97339
4
+ data.tar.gz: 738b2dd24a3b76c577f415e17517558912ce4c26
5
+ SHA512:
6
+ metadata.gz: 39b2e31eff31f19eaa895f4db037a59879c53c38d8884a780bda2976470d3efc6d9747f67a21353ed82ed48545ee5821a57e9cff883553fcd196bef1c67b1907
7
+ data.tar.gz: dbec0ab755aefbc7d0c5a542804222dc8aae7e80341f56925671071a897d573e7352d8dc0e0aca8b47ccabb38f1008a63389069700229685f79207d6165edec4
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ source 'https://rubygems.org'
5
+
6
+ # Specify your gem's dependencies in activerecord-human-id.gemspec
7
+ gemspec
8
+
9
+ gem 'essay', path: '~/Development/essay'
10
+ gem 'activerecord-traits', path: '~/Development/activerecord-traits'
11
+ gem 'unicode-tools', path: '~/Development/unicode-tools'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Yaroslav Konoplov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ ```ruby
2
+ class Article < ActiveRecord::Base
3
+ # TODO Write example
4
+ end
5
+ ```
6
+
7
+ ## Gemfile
8
+ ```ruby
9
+ gem 'activerecord-human-id', github: 'yivo/activerecord-human-id'
10
+ ```
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/gem_tasks'
data/humanid.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'humanid'
6
+ s.version = '1.0.1'
7
+ s.authors = ['Yaroslav Konoplov']
8
+ s.email = ['eahome00@gmail.com']
9
+ s.summary = 'ActiveRecord human id'
10
+ s.description = 'ActiveRecord human id'
11
+ s.homepage = 'http://github.com/yivo/activerecord-human-id'
12
+ s.license = 'MIT'
13
+
14
+ s.executables = `git ls-files -z -- bin/*`.split("\x0").map{ |f| File.basename(f) }
15
+ s.files = `git ls-files -z`.split("\x0")
16
+ s.test_files = `git ls-files -z -- {test,spec,features}/*`.split("\x0")
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_dependency 'activesupport', '>= 3.0', '< 6.0'
20
+ s.add_dependency 'activerecord', '>= 3.0', '< 6.0'
21
+ s.add_dependency 'activerecord-traits', '~> 1.1'
22
+ s.add_dependency 'unicode-tools', '~> 1.0'
23
+ s.add_dependency 'rails-i18n'
24
+ end
data/lib/humanid.rb ADDED
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'active_support/all'
5
+ require 'active_record'
6
+ require 'rails-i18n'
7
+ require 'activerecord-traits'
8
+ require 'unicode-tools'
9
+
10
+ require 'humanid/canonicalization'
11
+ require 'humanid/transliteration'
12
+ require 'humanid/extension'
13
+ require 'humanid/extension/configuration'
14
+ require 'humanid/extension/builder'
15
+ require 'humanid/extension/pattern'
16
+ require 'humanid/extension/persistence'
17
+ require 'humanid/extension/generation'
18
+ require 'humanid/extension/validation'
19
+ require 'humanid/migration'
20
+ require 'humanid/railtie'
21
+
22
+ begin
23
+ require 'essay'
24
+ require 'humanid/essay'
25
+ rescue LoadError
26
+ end
27
+
28
+ module HumanID
29
+ class << self
30
+ def engine(engine = :transliteration)
31
+ engine == :canonicalization ? Canonicalization : Transliteration
32
+ end
33
+ end
34
+ end
35
+
36
+ class ActiveRecord::Base
37
+ include HumanID::Extension
38
+ end
39
+
40
+ class ActiveRecord::ConnectionAdapters::TableDefinition
41
+ include HumanID::ColumnTypes
42
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Canonicalization
6
+ class << self
7
+ #
8
+ # HumanID::Canonicalization.perform('Well-known English writer')
9
+ # => 'Well-known_English_writer'
10
+ #
11
+ # HumanID::Canonicalization.perform('Пушкин, Александр Сергеевич')
12
+ # => 'Пушкин,_Александр_Сергеевич'
13
+ #
14
+ def perform(str)
15
+ separator = behaviour.separator
16
+
17
+ str = str.join(separator) if str.is_a?(Array)
18
+
19
+ # This doesn't require comments
20
+ str = UnicodeTools.strip_bidi_override_chars(str)
21
+
22
+ # Replace all whitespace characters (including leading,
23
+ # trailing and inner) with HumanID.separator
24
+ str = UnicodeTools.replace_whitespace(str, separator)
25
+
26
+ # Fix for Rails format in routes
27
+ # http://coding-journal.com/rails-3-routing-parameters-with-dots/
28
+ str = str.tr('.', separator)
29
+
30
+ # Strip leading and trailing separators
31
+ str = str.gsub(surrounding_separators_regex, '')
32
+
33
+ # Replace two or more separator sequence with one separator: '__' => '_'
34
+ str.gsub(separator_sequence_regex, separator)
35
+ end
36
+
37
+ def valid?(human_id)
38
+ human_id = human_id.to_s if human_id.kind_of?(Symbol)
39
+ human_id.kind_of?(String) &&
40
+ UnicodeTools.has_bidi_override?(human_id) == false &&
41
+ UnicodeTools.has_whitespace?(human_id) == false
42
+ end
43
+
44
+ def validate!(human_id)
45
+ raise MalformedHumanIDError unless valid?(human_id)
46
+ true
47
+ end
48
+
49
+ def behaviour
50
+ Behaviour.instance
51
+ end
52
+
53
+ attr_accessor :surrounding_separators_regex, :separator_sequence_regex
54
+ end
55
+
56
+ class Behaviour
57
+ include Singleton
58
+
59
+ attr_accessor :separator
60
+
61
+ def initialize
62
+ self.separator = '_'
63
+ end
64
+
65
+ def separator=(new_sep)
66
+ @separator = new_sep
67
+
68
+ # Rebuild separator regexps
69
+ HumanID::Canonicalization.surrounding_separators_regex = /(\A#{new_sep}+)|(#{new_sep}+\z)/
70
+ HumanID::Canonicalization.separator_sequence_regex = /#{new_sep}+/
71
+
72
+ new_sep
73
+ end
74
+ end
75
+
76
+ class MalformedHumanIDError < StandardError
77
+ def initialize
78
+ super 'Human ID is malformed'
79
+ end
80
+ end
81
+ end
82
+
83
+ class << self
84
+ def canonicalize(str)
85
+ Canonicalization.perform(str)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ class Essay::AttributeFeatures
5
+ def human_id?
6
+ active_record.attributes_marked_as_human_id.include?(attribute.name)
7
+ end
8
+
9
+ def human_id
10
+ @human_id || @human_id = HumanID.new(env) if human_id?
11
+ end
12
+
13
+ serialize do
14
+ { is_human_id: human_id?,
15
+ human_id: human_id.try(:to_hash) }
16
+ end
17
+
18
+ class HumanID < Base
19
+ def persists?
20
+ !!options[:persist]
21
+ end
22
+
23
+ def updates_manually?
24
+ options[:update] == :manual
25
+ end
26
+
27
+ def updates_automatically?
28
+ !updates_manually?
29
+ end
30
+
31
+ def updates_if_blank?
32
+ options[:update] == :if_blank
33
+ end
34
+
35
+ def options
36
+ active_record.human_id_options.fetch(attribute.name)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Extension
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :human_id_options,
10
+ instance_reader: true, instance_writer: false, instance_predicate: false
11
+
12
+ class_attribute :attributes_marked_as_human_id,
13
+ instance_accessor: false, instance_predicate: false
14
+
15
+ # Hash of options for each defined human id:
16
+ # => { alias: { pattern: [:id, :name], ... }, key: { pattern: [:id, :name], ... } }
17
+ self.human_id_options = {}
18
+
19
+ self.attributes_marked_as_human_id = []
20
+ end
21
+
22
+ module ClassMethods
23
+ def has_human_id(*args)
24
+ options = args.extract_options!.reverse_merge!(HumanID::Extension.default_options)
25
+
26
+ pattern = options.delete(:pattern) || Pattern.guess(self)
27
+ options[:original_pattern] = pattern
28
+ options[:compiled_pattern] = Pattern.compile(pattern)
29
+
30
+ val = options[:validations]
31
+ options[:validations] = case val
32
+ when Hash then val.reverse_merge!(HumanID::Extension.default_options[:validations])
33
+ when Array then %i( format uniqueness ).each_with_object({}) { |el, obj| obj[el] = val.include?(el) }
34
+ else { format: !!val, uniqueness: !!val }
35
+ end
36
+
37
+ builder = Builder.new(self, options)
38
+
39
+ self.human_id_options = self.human_id_options.dup
40
+ args.each do |attr_name|
41
+ builder.build(attr_name)
42
+ self.human_id_options[attr_name] = options.deep_dup
43
+ end
44
+
45
+ self.attributes_marked_as_human_id += args
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,101 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Extension
6
+ class Builder
7
+ def initialize(model_class, options)
8
+ @model = model_class
9
+ @param = options[:param]
10
+ @persists = options[:persist]
11
+ @validations = options[:validations]
12
+ @validates_format = @validations[:format]
13
+ @validates_uniqueness = @validations[:uniqueness]
14
+ end
15
+
16
+ def build(human_id)
17
+ @human_id = human_id
18
+
19
+ define_base_methods
20
+
21
+ if @persists
22
+ define_persistence_methods
23
+ add_persistence_callbacks
24
+ else
25
+ define_human_id_accessor
26
+ end
27
+
28
+ add_format_validation if @validates_format
29
+ add_uniqueness_validation if @validates_uniqueness
30
+
31
+ define_to_param_method if @param
32
+ end
33
+
34
+ protected
35
+ def define_base_methods
36
+ @model.class_eval <<-BODY, __FILE__, __LINE__ + 1
37
+ def generate_#{@human_id}
38
+ HumanID::Extension::Generation.generate(:#{@human_id}, self)
39
+ end
40
+
41
+ def options_for_#{@human_id}
42
+ human_id_options.fetch(:#{@human_id})
43
+ end
44
+ BODY
45
+ end
46
+
47
+ def define_persistence_methods
48
+ @model.class_eval <<-BODY, __FILE__, __LINE__ + 1
49
+ def need_to_update_#{@human_id}?
50
+ HumanID::Extension::Persistence.need_to_update?(:#{@human_id}, self)
51
+ end
52
+
53
+ def assign_#{@human_id}!
54
+ HumanID::Extension::Persistence.assign!(:#{@human_id}, self)
55
+ nil
56
+ end
57
+
58
+ def assign_#{@human_id}
59
+ HumanID::Extension::Persistence.assign(:#{@human_id}, self)
60
+ nil
61
+ end
62
+ BODY
63
+ end
64
+
65
+ def define_human_id_accessor
66
+ @model.class_eval <<-BODY, __FILE__, __LINE__ + 1
67
+ def #{@human_id}
68
+ @#{@human_id} ||= generate_#{@human_id}
69
+ end
70
+ BODY
71
+ end
72
+
73
+ def define_to_param_method
74
+ @model.class_eval <<-BODY, __FILE__, __LINE__ + 1
75
+ def to_param
76
+ self.#{@human_id}
77
+ end
78
+ BODY
79
+ end
80
+
81
+ def add_persistence_callbacks
82
+ @model.before_validation :"assign_#{@human_id}", if: :"need_to_update_#{@human_id}?"
83
+ @model.after_save :"assign_#{@human_id}!", if: :"need_to_update_#{@human_id}?"
84
+ end
85
+
86
+ def add_uniqueness_validation
87
+ @model.validates_uniqueness_of @human_id, case_sensitive: false, allow_blank: true
88
+ end
89
+
90
+ def add_format_validation
91
+ @model.validate "format_of_#{@human_id}", if: "#{@human_id}?"
92
+
93
+ @model.class_eval <<-BODY, __FILE__, __LINE__ + 1
94
+ def format_of_#{@human_id}
95
+ HumanID::Extension::Validation.validate_format_of(:#{@human_id}, self)
96
+ end
97
+ BODY
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Extension
6
+ class << self
7
+ attr_accessor :default_options
8
+ end
9
+
10
+ self.default_options = {
11
+
12
+ # By default pattern will be guessed by steps:
13
+ # - try first attribute with string type
14
+ # - or try primary key attribute
15
+ # - or at worst: first attribute
16
+ #
17
+ # See HumanID::Extension::PatternHelper.guess.
18
+ pattern: nil,
19
+
20
+ # If set to true: all available validations will be added.
21
+ # If set to false: no validations will be added.
22
+ # If set to hash or array: you choose what validations you need:
23
+ # => { format: true, uniqueness: false }
24
+ # => [:uniqueness]
25
+ validations: { format: true, uniqueness: true },
26
+
27
+ # This option determines whether `to_param` method will be defined.
28
+ # See http://apidock.com/rails/ActiveRecord/Integration/ClassMethods/to_param
29
+ param: false,
30
+
31
+ # Chose what engine should be used to perform human id generation.
32
+ engine: :transliteration,
33
+
34
+ # If you want your human id to be persisted in database set this to true.
35
+ persist: true,
36
+
37
+ # This option determines whether human id will be updated.
38
+ # Possible values:
39
+ # 1. :if_blank - human id will be updated only if it's value is blank;
40
+ # 2. :always - human id will be updated on each save regardless of the value;
41
+ # 3. :manual - human id will never be updated automatically.
42
+ update: :if_blank
43
+ }
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Extension
6
+ module Generation
7
+ class << self
8
+ def generate(human_id, model)
9
+ options = model.send("options_for_#{human_id}")
10
+ pattern = options.fetch(:compiled_pattern)
11
+ data = HumanID::Extension::Pattern.result(pattern, model)
12
+ HumanID.engine(options[:engine]).perform(data)
13
+ end
14
+
15
+ def ready_to_generate?(model)
16
+ # Sometimes human id depends on record id or timestamps.
17
+ # When record isn't persisted human id will be generated wrong.
18
+ model.persisted?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Extension
6
+ module Pattern
7
+ class << self
8
+ def compile(handy_pattern)
9
+ case handy_pattern
10
+ # compile(['Article', :name])
11
+ # => [{string: 'Article'}, {method: :name}]
12
+ when Array then handy_pattern
13
+
14
+ # compile(:name)
15
+ # => [{ method: :name }]
16
+ when Symbol then [{ method: handy_pattern }]
17
+
18
+ # compile(':id_:name')
19
+ # => [ [{method: :id}, {string: '_'}, {method: :name}] ]
20
+ #
21
+ # compile('_:article_name:id_')
22
+ # => [ [{string: '_'}, {method: :article_name}, {method: :id}, {string: '_'}] ]
23
+ #
24
+ # compile('article_:id')
25
+ # => [ [{string: 'article_'}, {method: :id}] ]
26
+ #
27
+ # http://rubular.com/r/AJFnJbmBRo
28
+ #
29
+ # http://stackoverflow.com/questions/16398471/regex-not-ending-with
30
+ # ?<! - not ending with
31
+ else [handy_pattern.scan(/([\w-]+)|(:\w+(?<![_-]))/).flatten.compact.map do |el|
32
+ el.start_with?(':') ? { method: el[1..-1].to_sym } : { string: el }
33
+ end]
34
+ end
35
+ end
36
+
37
+ # class CreateArticles < ActiveRecord::Migration
38
+ # def change
39
+ # create_table :articles do |t|
40
+ # t.string :name
41
+ # t.text :content
42
+ # end
43
+ # end
44
+ # end
45
+ #
46
+ # class Article < ActiveRecord::Base
47
+ # end
48
+ #
49
+ # guess(Article)
50
+ # => :name
51
+ #
52
+ def guess(model_class)
53
+ attrs = Traits.for(model_class).attributes
54
+ attr = attrs.find { |el| el.string? } || attrs.find { |el| el.primary_key? } || attrs.first
55
+ attr.name
56
+ end
57
+
58
+ def result(compiled_pattern, model)
59
+ compiled_pattern.map do |el|
60
+ case el
61
+ when Symbol then model.send(el)
62
+ when Hash then el.key?(:method) ? model.send(el[:method]) : el.fetch(:string)
63
+ when Array then result(el, model).join('')
64
+ else el
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Extension
6
+ module Persistence
7
+ class << self
8
+ def assign!(human_id, model)
9
+ if Generation.ready_to_generate?(model)
10
+ new_value = model.send("generate_#{human_id}")
11
+ if model.send(human_id) != new_value
12
+ model[human_id] = new_value
13
+ model.update_column(human_id, new_value)
14
+ model.save!
15
+ end
16
+ end
17
+ end
18
+
19
+ def assign(human_id, model)
20
+ # Sometimes human id depends on record id or timestamps.
21
+ # When record isn't persisted human id will be generated wrong.
22
+ if Generation.ready_to_generate?(model)
23
+ model.send("#{human_id}=", model.send("generate_#{human_id}"))
24
+ end
25
+ end
26
+
27
+ def need_to_update?(human_id, model)
28
+ options = model.send("options_for_#{human_id}")
29
+ case options[:update]
30
+ when :always then true
31
+ when nil, :if_blank then !model.send("#{human_id}?")
32
+ else false
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Extension
6
+ module Validation
7
+ class << self
8
+ def validate_format_of(human_id, model)
9
+ value = model.send(human_id)
10
+ options = model.send("options_for_#{human_id}")
11
+ valid = HumanID.engine(options[:engine]).valid?(value)
12
+ model.errors.add(human_id, :invalid) unless valid
13
+ valid
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Migration
6
+ def human_id(*args)
7
+ options = args.extract_options!
8
+ options[:index] = case options[:index]
9
+ when true then { unique: true }
10
+ when false then false
11
+ else (options[:index] || {}).reverse_merge!(unique: true)
12
+ end
13
+
14
+ args.each { |name| column(name, :string, options) }
15
+ end
16
+
17
+ # Commonly used
18
+ def alias(*args)
19
+ args << :alias if args.empty? || args.first.kind_of?(Hash)
20
+ human_id(*args)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ class Railtie < Rails::Railtie
6
+ # http://blog.nathanhumbert.com/2010/02/rails-3-loading-rake-tasks-from-gem.html
7
+ # TODO Fix bug: task runs twice
8
+ rake_tasks { load 'tasks/human_id.rake' }
9
+ end
10
+ end
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module HumanID
5
+ module Transliteration
6
+ class << self
7
+ #
8
+ # HumanID::Transliteration.perform('Well-known English writer')
9
+ # => 'well-known-english-writer'
10
+ #
11
+ # HumanID::Transliteration.perform('Well-known English writer', normalize: false, downcase: false)
12
+ # => 'Well-known English writer'
13
+ #
14
+ # HumanID::Transliteration.perform('Пушкин, Александр Сергеевич')
15
+ # => 'pushkin-aleksandr-sergeevich'
16
+ #
17
+ # HumanID::Transliteration.perform('Пушкин, Александр Сергеевич', normalize: false, downcase: false)
18
+ # => 'Pushkin, Aleksandr Sergeevich'
19
+ #
20
+ def perform(str, options = {})
21
+ previous_locale = I18n.locale
22
+ I18n.locale = I18n.default_locale
23
+ separator = options.fetch(:separator, behaviour.separator)
24
+ downcase = options.fetch(:downcase, behaviour.perform_downcase?)
25
+ normalize = options.fetch(:normalize, behaviour.perform_normalization?)
26
+
27
+ str = str.join(separator) if str.is_a?(Array)
28
+ str = I18n.transliterate(str)
29
+
30
+ str = str.downcase if downcase
31
+
32
+ if normalize
33
+ # Strip leading and trailing non-word and non-ASCII characters
34
+ str = str.gsub(/(\A\W+)|(\W+\z)/, '')
35
+
36
+ # Replace the rest of non-word and non-ASCII characters with hyphen
37
+ str = str.gsub(/\W+/, separator)
38
+ end
39
+
40
+ str
41
+ ensure
42
+ I18n.default_locale = previous_locale
43
+ end
44
+
45
+
46
+ def valid?(human_id)
47
+ human_id = human_id.to_s if human_id.kind_of?(Symbol)
48
+ human_id.kind_of?(String) && !!(human_id =~ behaviour.validation_regex)
49
+ end
50
+
51
+ def validate!(human_id)
52
+ raise MalformedHumanIDError unless valid?(human_id)
53
+ true
54
+ end
55
+
56
+ def behaviour
57
+ Behaviour.instance
58
+ end
59
+ end
60
+
61
+ class Behaviour
62
+ include Singleton
63
+
64
+ attr_accessor :separator, :downcase, :normalize, :validation_regex
65
+
66
+ def initialize
67
+ self.separator = '-'
68
+ self.downcase = true
69
+ self.normalize = true
70
+
71
+ # Not starts with hyphen
72
+ # Contains 1 to 255 word characters and hyphens
73
+ # Not ends with hyphen
74
+ # TODO Validation with custom separator
75
+ self.validation_regex = /(?!-)\A[\w-]{1,255}(?<!-)\z/
76
+ end
77
+
78
+ def perform_downcase?
79
+ downcase
80
+ end
81
+
82
+ def perform_normalization?
83
+ normalize
84
+ end
85
+ end
86
+
87
+ class MalformedHumanIDError < StandardError
88
+ def initialize
89
+ super 'Human ID is malformed'
90
+ end
91
+ end
92
+ end
93
+
94
+ class << self
95
+ def transliterate(str, options = {})
96
+ Transliteration.perform(str, options)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+ task 'human_id:regenerate' => :environment do
2
+ force = [ENV['force'], ENV['FORCE']].include?('true')
3
+
4
+ puts "Force #{force ? 'true' : 'false'}"
5
+
6
+ Traits.each do |traits|
7
+ attributes = traits.attributes.select do |attr|
8
+ human_id = attr.features.human_id
9
+ human_id && human_id.persists? && human_id.updates_automatically?
10
+ end
11
+
12
+ next if attributes.empty?
13
+
14
+ traits.active_record.find_each do |model|
15
+ puts "Processing #{traits.class_name}##{traits.primary_key_attribute.value_from(model)}"
16
+ changes = 0
17
+ attributes.each do |attr|
18
+ current = attr.value_from(model)
19
+
20
+ next if !force && !model.send("need_to_update_#{attr.name}?")
21
+
22
+ new = model.send("generate_#{attr.name}")
23
+
24
+ if current != new
25
+ changes += 1
26
+ puts "Current #{attr.name}: #{current ? "'#{current}'" : 'nil'}"
27
+ puts "New #{attr.name}: #{new ? "'#{new}'" : 'nil'}"
28
+
29
+ model.send("assign_#{attr.name}")
30
+ else
31
+ puts "#{attr.name} didn't change"
32
+ end
33
+
34
+ puts
35
+ end
36
+ model.save! if changes > 0
37
+ end
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: humanid
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yaroslav Konoplov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activerecord
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '6.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '6.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: activerecord-traits
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.1'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.1'
67
+ - !ruby/object:Gem::Dependency
68
+ name: unicode-tools
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.0'
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rails-i18n
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ description: ActiveRecord human id
96
+ email:
97
+ - eahome00@gmail.com
98
+ executables: []
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - ".gitignore"
103
+ - Gemfile
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - humanid.gemspec
108
+ - lib/humanid.rb
109
+ - lib/humanid/canonicalization.rb
110
+ - lib/humanid/essay.rb
111
+ - lib/humanid/extension.rb
112
+ - lib/humanid/extension/builder.rb
113
+ - lib/humanid/extension/configuration.rb
114
+ - lib/humanid/extension/generation.rb
115
+ - lib/humanid/extension/pattern.rb
116
+ - lib/humanid/extension/persistence.rb
117
+ - lib/humanid/extension/validation.rb
118
+ - lib/humanid/migration.rb
119
+ - lib/humanid/railtie.rb
120
+ - lib/humanid/transliteration.rb
121
+ - lib/tasks/human_id.rake
122
+ homepage: http://github.com/yivo/activerecord-human-id
123
+ licenses:
124
+ - MIT
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 2.5.1
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: ActiveRecord human id
146
+ test_files: []