guise 0.6.0 → 0.6.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.
data/guise.gemspec CHANGED
@@ -19,22 +19,17 @@ Gem::Specification.new do |gem|
19
19
  gem.version = Guise::VERSION
20
20
  gem.license = 'MIT'
21
21
 
22
- gem.required_ruby_version = '>= 1.9.3'
22
+ gem.required_ruby_version = ">= 2.0.0"
23
23
 
24
24
  gem.add_dependency "activerecord", ">= 3.1", "< 5.0"
25
25
  gem.add_dependency "activesupport", ">= 3.1", "< 5.0"
26
- gem.add_development_dependency 'rake', '~> 10.1'
26
+ gem.add_development_dependency "appraisal", ">= 1.0"
27
+ gem.add_development_dependency "byebug", "~> 5.0"
28
+ gem.add_development_dependency "codeclimate-test-reporter", "~> 0.3"
29
+ gem.add_development_dependency "pry", "~> 0.9"
30
+ gem.add_development_dependency "rake", "~> 10.1"
31
+ gem.add_development_dependency "redcarpet", "~> 3.2"
27
32
  gem.add_development_dependency "rspec", "~> 3.0"
28
- gem.add_development_dependency "factory_girl", "~> 4.4"
29
- gem.add_development_dependency "shoulda-matchers", "~> 2.5"
30
- gem.add_development_dependency "appraisal", '~> 0.5'
31
- gem.add_development_dependency 'pry', '~> 0.9'
32
- gem.add_development_dependency 'codeclimate-test-reporter', '~> 0.3'
33
-
34
- if RUBY_PLATFORM == 'java'
35
- gem.add_development_dependency 'activerecord-jdbcsqlite3-adapter'
36
- gem.add_development_dependency 'jdbc-sqlite3'
37
- else
38
- gem.add_development_dependency 'sqlite3', '~> 1.3'
39
- end
33
+ gem.add_development_dependency "sqlite3", "~> 1.3"
34
+ gem.add_development_dependency "yard", "~> 0.8"
40
35
  end
data/lib/guise.rb CHANGED
@@ -1,15 +1,39 @@
1
+ require "active_support/core_ext/string/inflections"
2
+ require "active_support/core_ext/array/extract_options"
1
3
  require 'active_support/core_ext/module/attribute_accessors'
4
+ require "active_support/lazy_load_hooks"
2
5
 
3
6
  require 'guise/version'
7
+ require "guise/errors"
8
+ require "guise/registry"
9
+ require "guise/options"
10
+ require "guise/builders"
11
+ require "guise/scopes"
4
12
  require 'guise/callbacks'
5
- require 'guise/syntax'
6
- require 'guise/introspection'
13
+ require "guise/introspection"
14
+ require "guise/syntax"
7
15
 
8
16
  module Guise
9
17
  mattr_reader :registry
10
- @@registry = HashWithIndifferentAccess.new
18
+ @@registry = Registry.new
19
+
20
+ DEFAULT_ASSOCIATION_NAME = "guises"
21
+ DEFAULT_ATTRIBUTE_NAME = "value"
22
+
23
+ def self.register_source(source_class, *guises)
24
+ options = Options.new(source_class, *guises)
25
+ registry[source_class.name] = options
26
+
27
+ HasGuisesBuilder.new(options).build!
28
+ end
29
+
30
+ def self.register_association(association_class, source_class_name, association_options)
31
+ options = registry[source_class_name]
32
+
33
+ GuiseForBuilder.new(association_class, options, association_options).build!
34
+ end
11
35
  end
12
36
 
13
- if defined?(ActiveRecord)
14
- ActiveRecord::Base.extend(Guise::Syntax)
37
+ ActiveSupport.on_load(:active_record) do
38
+ extend Guise::Syntax
15
39
  end
@@ -0,0 +1,129 @@
1
+ require "guise/scopes"
2
+
3
+ module Guise
4
+ # @api private
5
+ class HasGuisesBuilder
6
+ def initialize(options)
7
+ @options = options
8
+ end
9
+
10
+ def build!
11
+ set_guise_options!
12
+ define_association!
13
+ define_scopes!
14
+
15
+ if @options.association_options != DEFAULT_ASSOCIATION_NAME
16
+ define_association_aliases!
17
+ end
18
+
19
+ define_introspection_aliases!
20
+ end
21
+
22
+ private
23
+
24
+ def set_guise_options!
25
+ source_class.class_attribute :guise_options, instance_writer: false
26
+ source_class.guise_options = @options
27
+ end
28
+
29
+ def define_association!
30
+ source_class.has_many(@options.association_name, @options.association_options)
31
+ end
32
+
33
+ def define_scopes!
34
+ @options.values.each do |value|
35
+ method_name = value.underscore
36
+ scope_name = method_name.pluralize
37
+
38
+ source_class.scope scope_name, HasGuisesScope.new(value, @options)
39
+
40
+ source_class.class_eval <<-METHOD
41
+ def #{method_name}?
42
+ has_guise?('#{value}')
43
+ end
44
+ METHOD
45
+ end
46
+ end
47
+
48
+ def define_association_aliases!
49
+ source_class.class_eval <<-ASSOCIATION_ALIASES
50
+ alias_method :guises, :#{association_name}
51
+ alias_method :guises=, :#{association_name}=
52
+ alias_method :guise_ids, :#{association_name_singular}_ids
53
+ alias_method :guise_ids=, :#{association_name_singular}_ids=
54
+ ASSOCIATION_ALIASES
55
+ end
56
+
57
+ def define_introspection_aliases!
58
+ source_class.class_eval <<-INTROSPECTION_ALIASES
59
+ alias_method :has_#{association_name_singular}?, :has_guise?
60
+ alias_method :has_#{association_name}?, :has_guises?
61
+ alias_method :has_any_#{association_name}?, :has_any_guises?
62
+ INTROSPECTION_ALIASES
63
+ end
64
+
65
+ def source_class
66
+ @options.source_class
67
+ end
68
+
69
+ def association_name
70
+ @options.association_name
71
+ end
72
+
73
+ def association_name_singular
74
+ @options.association_name_singular
75
+ end
76
+ end
77
+
78
+ # @api private
79
+ class GuiseForBuilder
80
+ def initialize(association_class, options, association_options)
81
+ @association_class = association_class
82
+ @options = options
83
+ @association_options = association_options.reverse_merge!(@options.default_association_options)
84
+ @define_validations = !@association_options.delete(:validate)
85
+ end
86
+
87
+ def build!
88
+ update_guise_options!
89
+ define_association!
90
+ define_scopes!
91
+
92
+ if define_validations?
93
+ define_validations!
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def define_validations?
100
+ @define_validations
101
+ end
102
+
103
+ def update_guise_options!
104
+ @options.association_class = @association_class
105
+ end
106
+
107
+ def define_association!
108
+ @association_class.belongs_to(@options.source_association_name, @association_options)
109
+ end
110
+
111
+ def define_scopes!
112
+ @options.values.each do |value|
113
+ @association_class.scope(
114
+ value.underscore.pluralize,
115
+ GuiseForScope.new(value, @options)
116
+ )
117
+ end
118
+ end
119
+
120
+ def define_validations!
121
+ @association_class.validates(
122
+ @options.attribute,
123
+ uniqueness: { scope: @options.association_options[:foreign_key] },
124
+ presence: true,
125
+ inclusion: { in: @options.values }
126
+ )
127
+ end
128
+ end
129
+ end
@@ -1,4 +1,5 @@
1
1
  module Guise
2
+ # @api private
2
3
  class Callback
3
4
  def initialize(guise, attribute)
4
5
  @guise = guise
@@ -6,6 +7,7 @@ module Guise
6
7
  end
7
8
  end
8
9
 
10
+ # @api private
9
11
  class SourceCallback < Callback
10
12
  def after_initialize(record)
11
13
  if record.new_record?
@@ -14,6 +16,7 @@ module Guise
14
16
  end
15
17
  end
16
18
 
19
+ # @api private
17
20
  class AssociationCallback < Callback
18
21
  def after_initialize(record)
19
22
  record.assign_attributes(@attribute => @guise)
@@ -0,0 +1,32 @@
1
+ module Guise
2
+ class DefinitionNotFound < StandardError
3
+ def initialize(name)
4
+ @name = name
5
+ end
6
+
7
+ def message
8
+ "no guises defined for #{@name.inspect}"
9
+ end
10
+ end
11
+
12
+ class DuplicateDefinition < StandardError
13
+ def initialize(name)
14
+ @name = name
15
+ end
16
+
17
+ def message
18
+ "guise definition for #{@name.inspect} already exists"
19
+ end
20
+ end
21
+
22
+ class InvalidGuiseValue < ArgumentError
23
+ def initialize(guise_value, klass)
24
+ @guise_value = guise_value
25
+ @klass = klass
26
+ end
27
+
28
+ def message
29
+ "`#{guise_value}' is not a defined guise value for #{klass}"
30
+ end
31
+ end
32
+ end
@@ -1,31 +1,43 @@
1
- require 'active_support/concern'
1
+ require "active_support/core_ext/string/inflections"
2
2
 
3
3
  module Guise
4
+ # {Introspection} handles checking if a record has one or more `guise` records
5
+ # associated with it.
4
6
  module Introspection
5
- extend ActiveSupport::Concern
6
-
7
+ # Checks if the record has a `guise` record identified by on the specified
8
+ # `value`.
9
+ #
10
+ # @param [String, Class, Symbol] value `guise` to check
11
+ # @return [true, false]
7
12
  def has_guise?(value)
8
13
  value = value.to_s.classify
9
14
 
10
- unless guise_options[:names].any? { |name| name == value }
15
+ unless guise_options.values.include?(value)
11
16
  raise ArgumentError, "no such guise #{value}"
12
17
  end
13
18
 
14
- guises.any? { |guise| !guise.marked_for_destruction? && guise[guise_options[:attribute]] == value }
19
+ association(guise_options.association_name).reader.any? do |record|
20
+ !record.marked_for_destruction? &&
21
+ record[guise_options.attribute] == value
22
+ end
15
23
  end
16
24
 
25
+ # Checks if the record has any `guise` records with identified by any of
26
+ # the specified `values`.
27
+ #
28
+ # @param [Array<String, Class, Symbol>] value `guise` to check
29
+ # @return [true, false]
17
30
  def has_any_guises?(*values)
18
31
  values.any? { |value| has_guise?(value) }
19
32
  end
20
33
 
34
+ # Checks if the record has `guise` records for all of the specified
35
+ # `values`.
36
+ #
37
+ # @param [Array<String, Class, Symbol>] value `guise` to check
38
+ # @return [true, false]
21
39
  def has_guises?(*values)
22
40
  values.all? { |value| has_guise?(value) }
23
41
  end
24
-
25
- private
26
-
27
- def guise_options
28
- Guise.registry[self.class.table_name.classify]
29
- end
30
42
  end
31
43
  end
@@ -0,0 +1,75 @@
1
+ module Guise
2
+ # @api private
3
+ class Options
4
+ attr_reader(
5
+ :source_class,
6
+ :values,
7
+ :association_name,
8
+ :association_name_singular,
9
+ :attribute,
10
+ :association_options,
11
+ :scopes
12
+ )
13
+
14
+ attr_writer :association_class
15
+
16
+ def initialize(source_class, *values)
17
+ options = values.extract_options!
18
+
19
+ if values.empty?
20
+ raise ArgumentError, "must specify values in `has_guises`"
21
+ end
22
+
23
+ @source_class = source_class
24
+ @values = values.map(&:to_s).to_set
25
+ @association_name =
26
+ options.delete(:association) || DEFAULT_ASSOCIATION_NAME
27
+ @association_name_singular = @association_name.to_s.singularize
28
+ @attribute = options.delete(:attribute) || DEFAULT_ATTRIBUTE_NAME
29
+ @association_options = options.reverse_merge!(default_association_options)
30
+
31
+ @scopes = values.inject(HashWithIndifferentAccess.new) do |all, value|
32
+ all.merge!(value => {})
33
+ end
34
+ @scopes.freeze
35
+ end
36
+
37
+ def scope(guise_value, scope_type)
38
+ value_scopes = @scopes.fetch(guise_value) do
39
+ raise InvalidGuiseValue.new(guise_value, source_class)
40
+ end
41
+
42
+ value_scopes.fetch(scope_type) do
43
+ raise ArgumentError, "`#{scope_type}' is not a valid type of scope"
44
+ end
45
+ end
46
+
47
+ def register_scope(guise_value, scope)
48
+ value_scopes = @scopes.fetch(guise_value) do
49
+ raise InvalidGuiseValue.new(guise_value, source_class)
50
+ end
51
+
52
+ if value_scopes.key?(scope.type)
53
+ raise "`#{scope.type}' scope already defined for #{source_class}"
54
+ end
55
+
56
+ value_scopes[scope.type] = scope
57
+ end
58
+
59
+ def association_class
60
+ if defined?(@association_class)
61
+ @association_class
62
+ else
63
+ raise "`guise_for` was not called on the association class"
64
+ end
65
+ end
66
+
67
+ def source_association_name
68
+ source_class.model_name.singular.to_sym
69
+ end
70
+
71
+ def default_association_options
72
+ { foreign_key: "#{source_association_name}_id".to_sym }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ require "active_support/hash_with_indifferent_access"
2
+
3
+ module Guise
4
+ class Registry
5
+ def initialize
6
+ @registry = HashWithIndifferentAccess.new
7
+ end
8
+
9
+ def [](name)
10
+ @registry.fetch(name) do
11
+ raise DefinitionNotFound.new(name)
12
+ end
13
+ end
14
+
15
+ def []=(name, definition)
16
+ @registry[name] = definition
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,65 @@
1
+ module Guise
2
+ # @api private
3
+ class Scope
4
+ def initialize(value, options)
5
+ @value = value
6
+ @options = options
7
+
8
+ @options.register_scope(@value, self)
9
+ end
10
+ end
11
+
12
+ # @api private
13
+ class HasGuisesScope < Scope
14
+ def call
15
+ relation.create_with(association_name => [association_relation.new])
16
+ end
17
+
18
+ def type
19
+ :has_guises
20
+ end
21
+
22
+ private
23
+
24
+ def relation
25
+ source_class.
26
+ select(source_class.arel_table[Arel.star]).
27
+ joins(association_name).
28
+ merge(association_relation)
29
+ end
30
+
31
+ def source_class
32
+ @options.source_class
33
+ end
34
+
35
+ def association_name
36
+ @options.association_name
37
+ end
38
+
39
+ def association_relation
40
+ @options.scope(@value, :guise_for).call
41
+ end
42
+ end
43
+
44
+ # @api private
45
+ class GuiseOfScope < HasGuisesScope
46
+ def call
47
+ relation
48
+ end
49
+
50
+ def type
51
+ :guise_of
52
+ end
53
+ end
54
+
55
+ # @api private
56
+ class GuiseForScope < Scope
57
+ def call
58
+ @options.association_class.where(@options.attribute => @value)
59
+ end
60
+
61
+ def type
62
+ :guise_for
63
+ end
64
+ end
65
+ end