humanid 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []