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