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 +7 -0
- data/.gitignore +15 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +10 -0
- data/Rakefile +4 -0
- data/humanid.gemspec +24 -0
- data/lib/humanid.rb +42 -0
- data/lib/humanid/canonicalization.rb +88 -0
- data/lib/humanid/essay.rb +39 -0
- data/lib/humanid/extension.rb +49 -0
- data/lib/humanid/extension/builder.rb +101 -0
- data/lib/humanid/extension/configuration.rb +45 -0
- data/lib/humanid/extension/generation.rb +23 -0
- data/lib/humanid/extension/pattern.rb +71 -0
- data/lib/humanid/extension/persistence.rb +38 -0
- data/lib/humanid/extension/validation.rb +18 -0
- data/lib/humanid/migration.rb +23 -0
- data/lib/humanid/railtie.rb +10 -0
- data/lib/humanid/transliteration.rb +99 -0
- data/lib/tasks/human_id.rake +39 -0
- metadata +146 -0
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
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
data/Rakefile
ADDED
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: []
|