Skip to content

Commit aaf2aea

Browse files
authored
Merge pull request #481 from Dynamoid/add-field-aliases
Add field aliases
2 parents 497d8b3 + a047a0a commit aaf2aea

File tree

6 files changed

+164
-40
lines changed

6 files changed

+164
-40
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,26 @@ field :actions_taken, :integer, default: 0
365365
field :joined_at, :datetime, default: -> { Time.now }
366366
```
367367

368+
#### Aliases
369+
370+
It might be helpful to define an alias for already existing field when
371+
naming convention used for a table differs from conventions common in
372+
Ruby:
373+
374+
```ruby
375+
field firstName, :string, alias: :first_name
376+
```
377+
378+
This way there will be generated
379+
setters/getters/`<name>?`/`<name>_before_type_cast` methods for both
380+
original field name (`firstName`) and an alias (`first_name`).
381+
382+
```ruby
383+
user = User.new(first_name: 'Michael')
384+
user.first_name # => 'Michael'
385+
user.firstName # => 'Michael'
386+
```
387+
368388
#### Custom Types
369389

370390
To use a custom type for a field, suppose you have a `Money` type.
@@ -632,7 +652,7 @@ u.email = '[email protected]'
632652
u.save
633653
```
634654

635-
Save forces persistence to the datastore: a unique ID is also assigned,
655+
Save forces persistence to the data store: a unique ID is also assigned,
636656
but it is a string and not an auto-incrementing number.
637657

638658
```ruby

lib/dynamoid/fields.rb

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'dynamoid/fields/declare'
4+
35
module Dynamoid
46
# All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
57
# specified with field, then they will be ignored.
@@ -43,15 +45,15 @@ module ClassMethods
4345
# end
4446
#
4547
# Its type determines how it is coerced when read in and out of the
46-
# datastore. You can specify +string+, +integer+, +number+, +set+, +array+,
48+
# data store. You can specify +string+, +integer+, +number+, +set+, +array+,
4749
# +map+, +datetime+, +date+, +serialized+, +raw+, +boolean+ and +binary+
4850
# or specify a class that defines a serialization strategy.
4951
#
5052
# By default field type is +string+.
5153
#
5254
# Set can store elements of the same type only (it's a limitation of
53-
# DynamoDB itself). If a set should store elements only some particular
54-
# type +of+ option should be specified:
55+
# DynamoDB itself). If a set should store elements only of some particular
56+
# type then +of+ option should be specified:
5557
#
5658
# field :hobbies, :set, of: :string
5759
#
@@ -126,41 +128,31 @@ module ClassMethods
126128
# user.age # => 21 - integer
127129
# user.age_before_type_cast # => '21' - string
128130
#
131+
# There is also an option +alias+ which allows to use another name for a
132+
# field:
133+
#
134+
# class User
135+
# include Dynamoid::Document
136+
#
137+
# field :firstName, :string, alias: :first_name
138+
# end
139+
#
140+
# user = User.new(firstName: 'Michael')
141+
# user.firstName # Michael
142+
# user.first_name # Michael
143+
#
129144
# @param name [Symbol] name of the field
130145
# @param type [Symbol] type of the field (optional)
131146
# @param options [Hash] any additional options for the field type (optional)
132147
#
133148
# @since 0.2.0
134149
def field(name, type = :string, options = {})
135-
named = name.to_s
136150
if type == :float
137151
Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
138152
type = :number
139153
end
140-
self.attributes = attributes.merge(name => { type: type }.merge(options))
141-
142-
# should be called before `define_attribute_methods` method because it defines a getter itself
143-
warn_about_method_overriding(name, name)
144-
warn_about_method_overriding("#{named}=", name)
145-
warn_about_method_overriding("#{named}?", name)
146-
warn_about_method_overriding("#{named}_before_type_cast?", name)
147154

148-
define_attribute_method(name) # Dirty API
149-
150-
generated_methods.module_eval do
151-
define_method(named) { read_attribute(named) }
152-
define_method("#{named}?") do
153-
value = read_attribute(named)
154-
case value
155-
when true then true
156-
when false, nil then false
157-
else
158-
!value.nil?
159-
end
160-
end
161-
define_method("#{named}=") { |value| write_attribute(named, value) }
162-
define_method("#{named}_before_type_cast") { read_attribute_before_type_cast(named) }
163-
end
155+
Dynamoid::Fields::Declare.new(self, name, type, options).call
164156
end
165157

166158
# Declare a table range key.
@@ -273,21 +265,14 @@ def timestamps_enabled?
273265
options[:timestamps] || (options[:timestamps].nil? && Dynamoid::Config.timestamps)
274266
end
275267

276-
private
277-
268+
# @private
278269
def generated_methods
279270
@generated_methods ||= begin
280271
Module.new.tap do |mod|
281272
include(mod)
282273
end
283274
end
284275
end
285-
286-
def warn_about_method_overriding(method_name, field_name)
287-
if instance_methods.include?(method_name.to_sym)
288-
Dynamoid.logger.warn("Method #{method_name} generated for the field #{field_name} overrides already existing method")
289-
end
290-
end
291276
end
292277

293278
# You can access the attributes of an object directly on its attributes method, which is by default an empty hash.

lib/dynamoid/fields/declare.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
3+
module Dynamoid
4+
module Fields
5+
# @private
6+
class Declare
7+
def initialize(source, name, type, options)
8+
@source = source
9+
@name = name.to_sym
10+
@type = type
11+
@options = options
12+
end
13+
14+
def call
15+
# Register new field metadata
16+
@source.attributes = @source.attributes.merge(
17+
@name => { type: @type }.merge(@options)
18+
)
19+
20+
# Should be called before `define_attribute_methods` method because it
21+
# defines an attribute getter itself
22+
warn_about_method_overriding
23+
24+
# Dirty API
25+
@source.define_attribute_method(@name)
26+
27+
# Generate getters and setters as well as other helper methods
28+
generate_instance_methods
29+
30+
# If alias name specified - generate the same instance methods
31+
if @options[:alias]
32+
generate_instance_methods_for_alias
33+
end
34+
end
35+
36+
private
37+
38+
def warn_about_method_overriding
39+
warn_if_method_exists(@name)
40+
warn_if_method_exists("#{@name}=")
41+
warn_if_method_exists("#{@name}?")
42+
warn_if_method_exists("#{@name}_before_type_cast?")
43+
end
44+
45+
def generate_instance_methods
46+
# only local variable is visible in `module_eval` block
47+
name = @name
48+
49+
@source.generated_methods.module_eval do
50+
define_method(name) { read_attribute(name) }
51+
define_method("#{name}?") do
52+
value = read_attribute(name)
53+
case value
54+
when true then true
55+
when false, nil then false
56+
else
57+
!value.nil?
58+
end
59+
end
60+
define_method("#{name}=") { |value| write_attribute(name, value) }
61+
define_method("#{name}_before_type_cast") { read_attribute_before_type_cast(name) }
62+
end
63+
end
64+
65+
def generate_instance_methods_for_alias
66+
# only local variable is visible in `module_eval` block
67+
name = @name
68+
69+
alias_name = @options[:alias].to_sym
70+
71+
@source.generated_methods.module_eval do
72+
alias_method alias_name, name
73+
alias_method "#{alias_name}=", "#{name}="
74+
alias_method "#{alias_name}?", "#{name}?"
75+
alias_method "#{alias_name}_before_type_cast", "#{name}_before_type_cast"
76+
end
77+
end
78+
79+
def warn_if_method_exists(method)
80+
if @source.instance_methods.include?(method.to_sym)
81+
Dynamoid.logger.warn("Method #{method} generated for the field #{@name} overrides already existing method")
82+
end
83+
end
84+
end
85+
end
86+
end

lib/dynamoid/loadable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def load(attrs)
1010
end
1111
end
1212

13-
# Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
13+
# Reload an object from the database -- if you suspect the object has changed in the data store and you need those
1414
# changes to be reflected immediately, you would call this method. This is a consistent read.
1515
#
1616
# @return [Dynamoid::Document] the document this method was called on

lib/dynamoid/persistence.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
# encoding: utf-8
1414
module Dynamoid
15-
# # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
16-
# # values to be of the same type as when they were passed in, based on the fields in the class.
15+
# Persistence is responsible for dumping objects to and marshalling objects from the data store. It tries to reserialize
16+
# values to be of the same type as when they were passed in, based on the fields in the class.
1717
module Persistence
1818
extend ActiveSupport::Concern
1919

spec/dynamoid/fields_spec.rb

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,40 @@
66
let(:address) { Address.new }
77

88
describe '.field' do
9-
context 'generated method overrided existing one' do
9+
context 'when :alias option specified' do
10+
let(:klass) do
11+
new_class do
12+
field :Name, :string, alias: :name
13+
end
14+
end
15+
16+
it 'generates getter and setter for alias' do
17+
object = klass.new
18+
19+
object.Name = 'Alex'
20+
expect(object.name).to eq('Alex')
21+
22+
object.name = 'Michael'
23+
expect(object.name).to eq('Michael')
24+
end
25+
26+
it 'generates <name>? method' do
27+
object = klass.new
28+
29+
expect(object.name?).to eq false
30+
object.name = 'Alex'
31+
expect(object.name?).to eq true
32+
end
33+
34+
it 'generates <name>_before_type_cast method' do
35+
object = klass.new(name: :Alex)
36+
37+
expect(object.name).to eq 'Alex'
38+
expect(object.name_before_type_cast).to eq :Alex
39+
end
40+
end
41+
42+
context 'when new generated method overrides existing one' do
1043
let(:module_with_methods) do
1144
Module.new do
1245
def foo; end

0 commit comments

Comments
 (0)