active_entity 0.1.0 → 0.2.0
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 +4 -4
- data/CHANGELOG.md +2 -0
- data/README.md +37 -2
- data/lib/active_entity.rb +10 -0
- data/lib/active_entity/accessor.rb +21 -0
- data/lib/active_entity/attribute.rb +59 -11
- data/lib/active_entity/coercion.rb +10 -0
- data/lib/active_entity/conversion_definitions.rb +62 -0
- data/lib/active_entity/default_definitions.rb +57 -0
- data/lib/active_entity/errors.rb +13 -0
- data/lib/active_entity/identity.rb +3 -7
- data/lib/active_entity/typecasting.rb +37 -0
- data/lib/active_entity/version.rb +1 -1
- data/spec/active_entity/accessor_spec.rb +45 -0
- data/spec/active_entity/attribute_spec.rb +67 -27
- data/spec/active_entity/coercion_spec.rb +20 -0
- data/spec/active_entity/conversion_definitions_spec.rb +28 -0
- data/spec/active_entity/default_definitions_spec.rb +117 -0
- data/spec/active_entity/identity_spec.rb +8 -0
- data/spec/active_entity/typecasting_spec.rb +75 -0
- metadata +18 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f64f30d78308e4f5887107685f1c7801485138f
|
4
|
+
data.tar.gz: 477079a529608f27cc99ed5be0e6da67edfddd65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd7a1b494c16cd2c2fcc0717f1eebe40310abfed70e340e2ecccd650ee3fadcd1ec8fc1d65d04c7f45feecce50f0d7d3c1ac94a6fcbd1ba11f3e8cc07b938f16
|
7
|
+
data.tar.gz: 8f6cadc7b710a9de8a3c4d839fdd123b754ef85c77dccb32765eb58b4cf24899f8177c4d5571fd8911721d3b1eaa2e4e765cc7642c085ed1d32b7cf3d461b581
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ActiveEntity
|
2
2
|
|
3
|
-
|
3
|
+
An extension for Active Model to encourage implementing entity.
|
4
4
|
|
5
5
|
[](https://travis-ci.org/taiki45/active_entity) [](https://coveralls.io/r/taiki45/active_entity) [](https://codeclimate.com/github/taiki45/active_entity) [](http://badge.fury.io/rb/active_entity)
|
6
6
|
|
@@ -23,9 +23,10 @@ Or install it yourself as:
|
|
23
23
|
## Synopsis
|
24
24
|
|
25
25
|
```ruby
|
26
|
+
# = Define model attributes with accessor and define identities =
|
26
27
|
class Message
|
27
28
|
include ActiveModel::Model
|
28
|
-
include ActiveEntity::
|
29
|
+
include ActiveEntity::Accessor
|
29
30
|
include ActiveEntity::Identity
|
30
31
|
|
31
32
|
attribute :title
|
@@ -47,6 +48,40 @@ expect(message.attributes).to eq({ "title" => "A README of ActiveEntity", "body"
|
|
47
48
|
|
48
49
|
another_messsage = Message.new(title: 'A README of ActiveEntity', body: '')
|
49
50
|
expect(message).to eq(another_messsage)
|
51
|
+
|
52
|
+
|
53
|
+
# = Coercion =
|
54
|
+
class Person
|
55
|
+
include ActiveModel::Model
|
56
|
+
include ActiveEntity::Accessor
|
57
|
+
include ActiveEntity::Coercion
|
58
|
+
|
59
|
+
attribute :name, type: String
|
60
|
+
attribute :age, type: Integer
|
61
|
+
end
|
62
|
+
|
63
|
+
alice = Person.new(name: 'Alice', age: '1')
|
64
|
+
expect(alice.name).to eq('Alice')
|
65
|
+
expect(alice.age).to eq(1)
|
66
|
+
|
67
|
+
|
68
|
+
# = Typecasting =
|
69
|
+
class Recipe
|
70
|
+
include ActiveModel::Model
|
71
|
+
include ActiveEntity::Accessor
|
72
|
+
include ActiveEntity::Typecasting
|
73
|
+
|
74
|
+
attribute :title, type: String
|
75
|
+
attribute :steps, type: Integer
|
76
|
+
attribute :likes, type: Integer
|
77
|
+
end
|
78
|
+
|
79
|
+
waffle = Recipe.new(title: 'Waffle', steps: '12', likes: 'abc')
|
80
|
+
expect { waffle.cast! }.to raise_error(ActiveEntity::CastError)
|
81
|
+
|
82
|
+
expect(waffle.title).to eq('Waffle')
|
83
|
+
expect(waffle.steps).to eq('12') #=> rollbacks casted value on error
|
84
|
+
expect(waffle.likes).to eq('abc')
|
50
85
|
```
|
51
86
|
|
52
87
|
## Contributing
|
data/lib/active_entity.rb
CHANGED
@@ -1,6 +1,16 @@
|
|
1
1
|
require 'active_model'
|
2
2
|
require 'active_support/concern'
|
3
|
+
require 'active_support/core_ext/string'
|
3
4
|
|
4
5
|
require 'active_entity/version'
|
6
|
+
require 'active_entity/errors'
|
5
7
|
require 'active_entity/attribute'
|
8
|
+
require 'active_entity/accessor'
|
9
|
+
require 'active_entity/typecasting'
|
10
|
+
require 'active_entity/coercion'
|
6
11
|
require 'active_entity/identity'
|
12
|
+
|
13
|
+
# TODO: to_param
|
14
|
+
|
15
|
+
require 'active_entity/conversion_definitions'
|
16
|
+
require 'active_entity/default_definitions'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveEntity
|
2
|
+
module Accessor
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :defined_attributes, instance_writer: false, instance_predicate: false
|
7
|
+
self.defined_attributes = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def attribute(name, options = {})
|
12
|
+
defined_attributes[name] = Attribute.new(name, options)
|
13
|
+
attr_accessor(name)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def attributes
|
18
|
+
Hash[defined_attributes.keys.map {|name| [name.to_s, public_send(name)] }]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,21 +1,69 @@
|
|
1
1
|
module ActiveEntity
|
2
|
-
|
3
|
-
|
2
|
+
# @example Cast with given type option.
|
3
|
+
# attr = ActiveEntity::Attribute.new(:user_id, type: Integer)
|
4
|
+
# attr.cast!('12') #=> 12
|
5
|
+
# attr.cast!('a') #=> raises ActiveEntity::CastError
|
6
|
+
#
|
7
|
+
# @example Cast with given casting procedure.
|
8
|
+
# attr = ActiveEntity::Attribute.new(
|
9
|
+
# :created_at,
|
10
|
+
# cast_by: -> (value) do
|
11
|
+
# case value
|
12
|
+
# when Fixnum, Float
|
13
|
+
# Time.at(value)
|
14
|
+
# when String
|
15
|
+
# Time.parse(value)
|
16
|
+
# else
|
17
|
+
# raise ActiveEntity::CastError.build(:Time, value)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# )
|
21
|
+
# attr.cast!(1420081200) #=> a Time object
|
22
|
+
# attr.cast!('2015/01/01 12:00:00') #=> a Time object
|
23
|
+
class Attribute
|
24
|
+
attr_reader :name, :options
|
4
25
|
|
5
|
-
|
6
|
-
|
7
|
-
|
26
|
+
# @param [Symbol] name
|
27
|
+
# @param [Hash{Symbol => Object}] options
|
28
|
+
def initialize(name, options = {})
|
29
|
+
raise ArgumentError unless options.is_a?(Hash)
|
30
|
+
@name, @options = name, options
|
8
31
|
end
|
9
32
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
33
|
+
# @return [Class, Symbol, String]
|
34
|
+
def type
|
35
|
+
@options[:type]
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Proc]
|
39
|
+
def cast_by
|
40
|
+
@options[:cast_by]
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Object] value
|
44
|
+
# @return [Object] casted value
|
45
|
+
# @raise [ActiveEntity::CastError]
|
46
|
+
def cast!(value)
|
47
|
+
case
|
48
|
+
when type
|
49
|
+
cast_by_defined_type(value)
|
50
|
+
when cast_by
|
51
|
+
cast_by.call(value)
|
52
|
+
else
|
53
|
+
value
|
14
54
|
end
|
15
55
|
end
|
16
56
|
|
17
|
-
|
18
|
-
|
57
|
+
private
|
58
|
+
|
59
|
+
def cast_by_defined_type(value)
|
60
|
+
procedure = ActiveEntity::ConversionDefinitions.get(type)
|
61
|
+
|
62
|
+
if procedure
|
63
|
+
procedure.call(value)
|
64
|
+
else
|
65
|
+
raise ConfigurationError.new("Can't find casting procedure for `#{type}`")
|
66
|
+
end
|
19
67
|
end
|
20
68
|
end
|
21
69
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ActiveEntity
|
2
|
+
class ConversionDefinitions
|
3
|
+
class << self
|
4
|
+
# @param [Class, Symbol, String] type
|
5
|
+
# @return [Proc]
|
6
|
+
def get(type)
|
7
|
+
type_casting_procs.get(type)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param [Class, Symbol, String] type
|
11
|
+
# @return [Proc]
|
12
|
+
def set(type, &block)
|
13
|
+
type_casting_procs.set(type, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Run block with new type_casting_procs. For test purpose.
|
17
|
+
# @return [nil]
|
18
|
+
def run_with_new_then_restore
|
19
|
+
old, @type_casting_procs = type_casting_procs, MapWithNormalizing.new
|
20
|
+
yield
|
21
|
+
@type_casting_procs = old
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def type_casting_procs
|
28
|
+
@type_casting_procs ||= MapWithNormalizing.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
class MapWithNormalizing
|
35
|
+
def initialize
|
36
|
+
@map = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def get(type)
|
40
|
+
@map[normalize_type(type)]
|
41
|
+
end
|
42
|
+
|
43
|
+
def set(type, &block)
|
44
|
+
# TODO: block arity check
|
45
|
+
@map[normalize_type(type)] = block
|
46
|
+
end
|
47
|
+
|
48
|
+
def normalize_type(type)
|
49
|
+
case type
|
50
|
+
when Class
|
51
|
+
type.to_s
|
52
|
+
when String
|
53
|
+
type
|
54
|
+
when Symbol
|
55
|
+
type.to_s
|
56
|
+
else
|
57
|
+
raise ArgumentError.new("`type` must be a Class or String or Symbol, but #{type.class}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
ActiveEntity::ConversionDefinitions.set(:Boolean) do |value|
|
2
|
+
case value
|
3
|
+
when 'true'
|
4
|
+
true
|
5
|
+
when 'false'
|
6
|
+
false
|
7
|
+
else
|
8
|
+
raise ActiveEntity::CastError.build(:Boolean, value)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
ActiveEntity::ConversionDefinitions.set(Integer) do |value|
|
13
|
+
begin
|
14
|
+
Integer(value)
|
15
|
+
rescue
|
16
|
+
raise ActiveEntity::CastError.build(Integer, value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
ActiveEntity::ConversionDefinitions.set(Float) do |value|
|
21
|
+
begin
|
22
|
+
Float(value)
|
23
|
+
rescue
|
24
|
+
raise ActiveEntity::CastError.build(Float, value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveEntity::ConversionDefinitions.set(:'ActiveSupport::TimeWithZone') do |value|
|
29
|
+
begin
|
30
|
+
case value
|
31
|
+
when Fixnum, Float
|
32
|
+
Time.zone.at(value)
|
33
|
+
when String
|
34
|
+
Time.zone.parse(value)
|
35
|
+
else
|
36
|
+
raise ActiveEntity::CastError.build(:'ActiveSupport::TimeWithZone', value)
|
37
|
+
end
|
38
|
+
rescue ArgumentError
|
39
|
+
raise ActiveEntity::CastError.build(:'ActiveSupport::TimeWithZone', value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ActiveEntity::ConversionDefinitions.set(String) do |value|
|
44
|
+
if value.respond_to? :to_s
|
45
|
+
value.to_s
|
46
|
+
else
|
47
|
+
raise ActiveEntity::CastError.build(String, value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
ActiveEntity::ConversionDefinitions.set(Symbol) do |value|
|
52
|
+
if value.respond_to? :to_sym
|
53
|
+
value.to_sym
|
54
|
+
else
|
55
|
+
raise ActiveEntity::CastError.build(Symbol, value)
|
56
|
+
end
|
57
|
+
end
|
@@ -3,8 +3,7 @@ module ActiveEntity
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
class_attribute :identity_attributes,
|
7
|
-
instance_writer: false, instance_predicate: false
|
6
|
+
class_attribute :identity_attributes, instance_writer: false, instance_predicate: false
|
8
7
|
self.identity_attributes = []
|
9
8
|
end
|
10
9
|
|
@@ -14,17 +13,14 @@ module ActiveEntity
|
|
14
13
|
|
15
14
|
define_method(:==) do |other|
|
16
15
|
return false unless self.class === other
|
17
|
-
|
18
|
-
names.all? do |name|
|
19
|
-
public_send(name) == other.public_send(name)
|
20
|
-
end
|
16
|
+
names.all? {|name| public_send(name) == other.public_send(name) }
|
21
17
|
end
|
22
18
|
end
|
23
19
|
end
|
24
20
|
|
25
21
|
# For ActiveModel::Conversion
|
26
22
|
def to_key
|
27
|
-
|
23
|
+
identity_attributes.empty? ? nil : identity_attributes.map {|name| public_send(name) }
|
28
24
|
end
|
29
25
|
end
|
30
26
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActiveEntity
|
2
|
+
module Typecasting
|
3
|
+
# Try to cast attribute values. When fails to cast, raises the error and
|
4
|
+
# rollbacks all values.
|
5
|
+
#
|
6
|
+
# @return [nil]
|
7
|
+
# @raise [ActiveEntity::CastError]
|
8
|
+
def cast!
|
9
|
+
casted_values = defined_attributes.map do |name, attr|
|
10
|
+
value = public_send(name)
|
11
|
+
next [name, nil] if value.nil?
|
12
|
+
[name, attr.cast!(value)]
|
13
|
+
end
|
14
|
+
|
15
|
+
casted_values.each {|name, casted| public_send("#{name}=", casted) }
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Try to cast attribute values. When fails to cast, ignores error and left
|
20
|
+
# attribute value as original one.
|
21
|
+
#
|
22
|
+
# @return [nil]
|
23
|
+
def cast
|
24
|
+
defined_attributes.each do |name, attr|
|
25
|
+
value = public_send(name)
|
26
|
+
next if value.nil?
|
27
|
+
|
28
|
+
begin
|
29
|
+
casted = attr.cast!(value)
|
30
|
+
public_send("#{name}=", casted)
|
31
|
+
rescue ActiveEntity::CastError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveEntity::Accessor do
|
4
|
+
let(:test_class) do
|
5
|
+
Class.new do
|
6
|
+
include ActiveModel::Model
|
7
|
+
include ActiveEntity::Accessor
|
8
|
+
|
9
|
+
attribute :name, type: String
|
10
|
+
attribute :age, type: String
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:test_attributes) { { name: 'Alice', age: 1 } }
|
15
|
+
|
16
|
+
describe '.defined_attributes' do
|
17
|
+
subject { test_class.defined_attributes }
|
18
|
+
it { is_expected.to be_kind_of(Hash) }
|
19
|
+
|
20
|
+
it 'returns all defined attributes' do
|
21
|
+
expect(subject.keys).to eq(%i(name age))
|
22
|
+
expect(subject[:name]).to be_kind_of(ActiveEntity::Attribute)
|
23
|
+
expect(subject[:age]).to be_kind_of(ActiveEntity::Attribute)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#initialize' do
|
28
|
+
subject { test_class.new(test_attributes) }
|
29
|
+
|
30
|
+
it 'assigns attributes' do
|
31
|
+
expect(subject.name).to eq('Alice')
|
32
|
+
expect(subject.age).to eq(1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#attributes' do
|
37
|
+
subject { test_class.new(test_attributes).attributes }
|
38
|
+
|
39
|
+
it { is_expected.to be_kind_of(Hash) }
|
40
|
+
|
41
|
+
it "returns all attribute's name-value pairs" do
|
42
|
+
is_expected.to eq('name' => 'Alice', 'age' => 1)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,43 +1,83 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ActiveEntity::Attribute do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
describe 'typecasting' do
|
5
|
+
context 'with type option' do
|
6
|
+
let(:casting_proc) do
|
7
|
+
-> (value) {
|
8
|
+
begin
|
9
|
+
Integer(value)
|
10
|
+
rescue
|
11
|
+
raise ActiveEntity::CastError.build(Integer, value)
|
12
|
+
end
|
13
|
+
}
|
14
|
+
end
|
8
15
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
16
|
+
before do
|
17
|
+
allow(ActiveEntity::ConversionDefinitions).to receive(:get).
|
18
|
+
with(Integer).and_return(casting_proc)
|
19
|
+
end
|
13
20
|
|
14
|
-
|
21
|
+
let(:attr) { ActiveEntity::Attribute.new(:user_id, type: Integer) }
|
15
22
|
|
16
|
-
|
17
|
-
|
18
|
-
|
23
|
+
context 'with valid value' do
|
24
|
+
it 'converts given value' do
|
25
|
+
expect(attr.cast!('12')).to be_a(Integer)
|
26
|
+
expect(attr.cast!('12')).to eq(12)
|
27
|
+
end
|
28
|
+
end
|
19
29
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
30
|
+
context 'with invalid value' do
|
31
|
+
it 'raises ActiveEntity::CastError' do
|
32
|
+
expect { attr.cast!('a') }.to raise_error(ActiveEntity::CastError)
|
33
|
+
end
|
34
|
+
end
|
24
35
|
|
25
|
-
|
26
|
-
|
36
|
+
context 'with procedure of type is not defined' do
|
37
|
+
before do
|
38
|
+
allow(ActiveEntity::ConversionDefinitions).to receive(:get).
|
39
|
+
with(:UnknownType).and_return(nil)
|
40
|
+
end
|
27
41
|
|
28
|
-
|
29
|
-
|
30
|
-
|
42
|
+
let(:attr) { ActiveEntity::Attribute.new(:user_id, type: :UnknownType) }
|
43
|
+
|
44
|
+
it 'raises ConfigurationError' do
|
45
|
+
expect {
|
46
|
+
attr.cast!('12')
|
47
|
+
}.to raise_error(ActiveEntity::ConfigurationError, /UnknownType/)
|
48
|
+
end
|
49
|
+
end
|
31
50
|
end
|
32
|
-
end
|
33
51
|
|
34
|
-
|
35
|
-
|
52
|
+
context 'with cast_by option' do
|
53
|
+
let(:attr) do
|
54
|
+
ActiveEntity::Attribute.new(
|
55
|
+
:created_at,
|
56
|
+
cast_by: -> (value) do
|
57
|
+
case value
|
58
|
+
when Fixnum, Float
|
59
|
+
Time.at(value)
|
60
|
+
when String
|
61
|
+
Time.parse(value)
|
62
|
+
else
|
63
|
+
raise ActiveEntity::CastError.build(:Time, value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'converts given value' do
|
70
|
+
expect(attr.cast!(1420081200)).to be_a(Time)
|
71
|
+
expect(attr.cast!('2015/01/01 12:00:00')).to be_a(Time)
|
72
|
+
end
|
73
|
+
end
|
36
74
|
|
37
|
-
|
75
|
+
context 'without typecasting options' do
|
76
|
+
let(:attr) { ActiveEntity::Attribute.new(:user_id) }
|
38
77
|
|
39
|
-
|
40
|
-
|
78
|
+
it 'does nothing and returns original' do
|
79
|
+
expect(attr.cast!('12')).to eq('12')
|
80
|
+
end
|
41
81
|
end
|
42
82
|
end
|
43
83
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveEntity::Coercion do
|
4
|
+
let(:test_class) do
|
5
|
+
Class.new do
|
6
|
+
include ActiveModel::Model
|
7
|
+
include ActiveEntity::Accessor
|
8
|
+
include ActiveEntity::Coercion
|
9
|
+
|
10
|
+
attribute :name, type: String
|
11
|
+
attribute :age, type: Integer
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'automatically typecasts attribute values' do
|
16
|
+
person = test_class.new(name: 'Alice', age: '1')
|
17
|
+
expect(person.name).to eq('Alice')
|
18
|
+
expect(person.age).to eq(1)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveEntity::ConversionDefinitions do
|
4
|
+
around do |example|
|
5
|
+
described_class.run_with_new_then_restore { example.run }
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '.set and .get' do
|
9
|
+
type_as_class = ActiveSupport::TimeWithZone
|
10
|
+
type_as_symbol = :'ActiveSupport::TimeWithZone'
|
11
|
+
type_as_string = 'ActiveSupport::TimeWithZone'
|
12
|
+
|
13
|
+
[type_as_class, type_as_symbol, type_as_string].permutation(2).each do |a, b|
|
14
|
+
context "when set with #{a.class} and get with #{b.class}" do
|
15
|
+
it 'returns valid procedure' do
|
16
|
+
described_class.set(a) {}
|
17
|
+
expect(described_class.get(b)).not_to be_nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when given invalid type' do
|
24
|
+
it 'raises ArgumentError' do
|
25
|
+
expect { described_class.set(1) }.to raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'default type conversion definitions' do
|
4
|
+
shared_examples 'raising error' do
|
5
|
+
it 'raises ActiveEntity::CastError' do
|
6
|
+
expect { subject }.to raise_error(ActiveEntity::CastError, /#{type}/)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:procedure) { ActiveEntity::ConversionDefinitions.get(type) }
|
11
|
+
subject { procedure.call(value) }
|
12
|
+
|
13
|
+
describe 'Boolean conversion' do
|
14
|
+
let(:type) { :Boolean }
|
15
|
+
|
16
|
+
context 'with `true` string' do
|
17
|
+
let(:value) { 'true' }
|
18
|
+
it { is_expected.to eq(true) }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'with `false` string' do
|
22
|
+
let(:value) { 'false' }
|
23
|
+
it { is_expected.to eq(false) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'with invalid value' do
|
27
|
+
let(:value) { 'True' }
|
28
|
+
include_examples 'raising error'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'Integer conversion' do
|
33
|
+
let(:type) { :Integer }
|
34
|
+
|
35
|
+
context 'with valid value' do
|
36
|
+
let(:value) { '12' }
|
37
|
+
it { is_expected.to eq(12) }
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with invalid value' do
|
41
|
+
let(:value) { 'abc' }
|
42
|
+
include_examples 'raising error'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'Float conversion' do
|
47
|
+
let(:type) { :Float }
|
48
|
+
|
49
|
+
context 'with valid value' do
|
50
|
+
let(:value) { '12.1' }
|
51
|
+
it { is_expected.to eq(12.1) }
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'with invalid value' do
|
55
|
+
let(:value) { 'abc' }
|
56
|
+
include_examples 'raising error'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'ActiveSupport::TimeWithZone conversion' do
|
61
|
+
around do |example|
|
62
|
+
back, Time.zone = Time.zone, 'UTC'
|
63
|
+
example.run
|
64
|
+
Time.zone = back
|
65
|
+
end
|
66
|
+
|
67
|
+
let(:type) { :'ActiveSupport::TimeWithZone' }
|
68
|
+
|
69
|
+
context 'with string expression' do
|
70
|
+
let(:value) { '2015/01/05 02:12:12' }
|
71
|
+
it { is_expected.to be_a(ActiveSupport::TimeWithZone) }
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'with float expression' do
|
75
|
+
let(:value) { 1420081200.089 }
|
76
|
+
it { is_expected.to be_a(ActiveSupport::TimeWithZone) }
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'with invalid string value' do
|
80
|
+
let(:value) { '1420081200.089' }
|
81
|
+
include_examples 'raising error'
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with invalid value' do
|
85
|
+
let(:value) { Time.now }
|
86
|
+
include_examples 'raising error'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'String conversion' do
|
91
|
+
let(:type) { :String }
|
92
|
+
|
93
|
+
context 'with valid value' do
|
94
|
+
let(:value) { 12 }
|
95
|
+
it { is_expected.to eq('12') }
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'with invalid value' do
|
99
|
+
let(:value) { Object.new.tap {|o| o.instance_eval { undef :to_s } } }
|
100
|
+
include_examples 'raising error'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'Symbol conversion' do
|
105
|
+
let(:type) { :Symbol }
|
106
|
+
|
107
|
+
context 'with valid value' do
|
108
|
+
let(:value) { 'abc' }
|
109
|
+
it { is_expected.to eq(:abc) }
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'with invalid value' do
|
113
|
+
let(:value) { 12 }
|
114
|
+
include_examples 'raising error'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveEntity::Typecasting do
|
4
|
+
let(:test_class) do
|
5
|
+
Class.new do
|
6
|
+
include ActiveModel::Model
|
7
|
+
include ActiveEntity::Accessor
|
8
|
+
include ActiveEntity::Typecasting
|
9
|
+
|
10
|
+
attribute :name, type: String
|
11
|
+
attribute :age, type: Integer
|
12
|
+
attribute :item_count, type: Integer
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:person) { test_class.new(assigned_values) }
|
17
|
+
let(:assigned_values) { { name: 'Alice', age: '1', item_count: '2' } }
|
18
|
+
|
19
|
+
describe '#cast!' do
|
20
|
+
context 'when assigned value is valid' do
|
21
|
+
it 'conversions its attributes' do
|
22
|
+
person.cast!
|
23
|
+
expect(person.name).to eq('Alice')
|
24
|
+
expect(person.age).to eq(1)
|
25
|
+
expect(person.item_count).to eq(2)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when assigned value is invalid' do
|
30
|
+
let(:assigned_values) { { name: 'Alice', age: '1', item_count: 'a' } }
|
31
|
+
|
32
|
+
it 'raises ActiveEntity::CastError' do
|
33
|
+
expect { person.cast! }.to raise_error(ActiveEntity::CastError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'rollbacks all values' do
|
37
|
+
expect { person.cast! }.to raise_error(ActiveEntity::CastError)
|
38
|
+
expect(person.age).to eq('1')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when assigned value is nil' do
|
43
|
+
let(:assigned_values) { { name: nil, age: '1', item_count: nil } }
|
44
|
+
|
45
|
+
it 'skips nil values' do
|
46
|
+
expect { person.cast! }.not_to raise_error
|
47
|
+
expect(person.name).to be_nil
|
48
|
+
expect(person.age).to eq(1)
|
49
|
+
expect(person.item_count).to be_nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#cast' do
|
55
|
+
context 'when assigned value is valid' do
|
56
|
+
it 'conversions its attributes' do
|
57
|
+
person.cast
|
58
|
+
expect(person.name).to eq('Alice')
|
59
|
+
expect(person.age).to eq(1)
|
60
|
+
expect(person.item_count).to eq(2)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when assigned value is invalid' do
|
65
|
+
let(:assigned_values) { { name: 'Alice', age: '1', item_count: 'a' } }
|
66
|
+
|
67
|
+
it 'skips invalid value' do
|
68
|
+
person.cast
|
69
|
+
expect(person.name).to eq('Alice')
|
70
|
+
expect(person.age).to eq(1)
|
71
|
+
expect(person.item_count).to eq('a')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_entity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taiki Ono
|
@@ -147,17 +147,29 @@ files:
|
|
147
147
|
- ".gitignore"
|
148
148
|
- ".rspec"
|
149
149
|
- ".travis.yml"
|
150
|
+
- CHANGELOG.md
|
150
151
|
- Gemfile
|
151
152
|
- LICENSE.txt
|
152
153
|
- README.md
|
153
154
|
- Rakefile
|
154
155
|
- active_entity.gemspec
|
155
156
|
- lib/active_entity.rb
|
157
|
+
- lib/active_entity/accessor.rb
|
156
158
|
- lib/active_entity/attribute.rb
|
159
|
+
- lib/active_entity/coercion.rb
|
160
|
+
- lib/active_entity/conversion_definitions.rb
|
161
|
+
- lib/active_entity/default_definitions.rb
|
162
|
+
- lib/active_entity/errors.rb
|
157
163
|
- lib/active_entity/identity.rb
|
164
|
+
- lib/active_entity/typecasting.rb
|
158
165
|
- lib/active_entity/version.rb
|
166
|
+
- spec/active_entity/accessor_spec.rb
|
159
167
|
- spec/active_entity/attribute_spec.rb
|
168
|
+
- spec/active_entity/coercion_spec.rb
|
169
|
+
- spec/active_entity/conversion_definitions_spec.rb
|
170
|
+
- spec/active_entity/default_definitions_spec.rb
|
160
171
|
- spec/active_entity/identity_spec.rb
|
172
|
+
- spec/active_entity/typecasting_spec.rb
|
161
173
|
- spec/readme_spec.rb
|
162
174
|
- spec/spec_helper.rb
|
163
175
|
homepage: https://github.com/taiki45/active_entity
|
@@ -185,8 +197,13 @@ signing_key:
|
|
185
197
|
specification_version: 4
|
186
198
|
summary: An Active Model extention for entity.
|
187
199
|
test_files:
|
200
|
+
- spec/active_entity/accessor_spec.rb
|
188
201
|
- spec/active_entity/attribute_spec.rb
|
202
|
+
- spec/active_entity/coercion_spec.rb
|
203
|
+
- spec/active_entity/conversion_definitions_spec.rb
|
204
|
+
- spec/active_entity/default_definitions_spec.rb
|
189
205
|
- spec/active_entity/identity_spec.rb
|
206
|
+
- spec/active_entity/typecasting_spec.rb
|
190
207
|
- spec/readme_spec.rb
|
191
208
|
- spec/spec_helper.rb
|
192
209
|
has_rdoc:
|