typed_fields 0.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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +80 -0
- data/Rakefile +1 -0
- data/lib/typed_fields/version.rb +3 -0
- data/lib/typed_fields.rb +119 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/typed_fields_spec.rb +137 -0
- data/typed_fields.gemspec +22 -0
- metadata +68 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Typed Fields
|
2
|
+
|
3
|
+
## Introduction
|
4
|
+
Using ActiveRecord to implement domain objects causes a lot of grief. Often it's just impossible as there is no table that you map your domain objects to.
|
5
|
+
|
6
|
+
ActiveModel is a big step forward and I highly recommend using it. It does a pretty good job doing what AR used to do but without coupling your domain objects to the database. One of a few things I miss though is typed fields. ActiveRecord takes care of all type conversations. You just pass a bunch of strings and it knows what to do with them. You need to do it manually if you use ActiveModel.
|
7
|
+
|
8
|
+
That's where the TypedFields gem comes into play. It allows you to specify types for your fields which eases the migration from ActiveRecord to ActiveModel.
|
9
|
+
|
10
|
+
|
11
|
+
## How to use TypedFields
|
12
|
+
```ruby
|
13
|
+
class Person
|
14
|
+
include TypedFields
|
15
|
+
|
16
|
+
string :first_name, :last_name
|
17
|
+
integer :age
|
18
|
+
decimal :income
|
19
|
+
|
20
|
+
def initialize params
|
21
|
+
initialize_fields params
|
22
|
+
end
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
As you can see from the example above including TypedFields adds several class methods (such as string, integer) and an instance method initializing fields.
|
27
|
+
|
28
|
+
## Advanced Features
|
29
|
+
|
30
|
+
### Using Custom Types
|
31
|
+
Besides having such basic types as integers, decimals, strings and booleans you can specify custom types.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
module UppercaseString
|
35
|
+
def self.parse str
|
36
|
+
str.upcase
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Person
|
41
|
+
include TypedFields
|
42
|
+
custom :name, type: UppercaseString
|
43
|
+
end
|
44
|
+
|
45
|
+
p = Person.new
|
46
|
+
p.initialize_fields name: "abc" # @name == ABC
|
47
|
+
```
|
48
|
+
|
49
|
+
### Arrays
|
50
|
+
```ruby
|
51
|
+
class Service
|
52
|
+
include TypedFields
|
53
|
+
array_of_integers :object_ids
|
54
|
+
end
|
55
|
+
|
56
|
+
s = Service.new
|
57
|
+
s.initialize_fields object_ids => ["1", "2"] # @object_ids == [1,2]
|
58
|
+
```
|
59
|
+
|
60
|
+
### Default Values
|
61
|
+
```ruby
|
62
|
+
class Person
|
63
|
+
include TypedFields
|
64
|
+
string :name
|
65
|
+
integer :age, default: 100
|
66
|
+
end
|
67
|
+
|
68
|
+
p = Person.new
|
69
|
+
p.initialize_fields name: "John" #@name == "John", @age == 100
|
70
|
+
```
|
71
|
+
|
72
|
+
### Using Proc as a Default Value
|
73
|
+
```ruby
|
74
|
+
class Person
|
75
|
+
include TypedFields
|
76
|
+
string :name, default: Proc.new{"default value"}
|
77
|
+
end
|
78
|
+
|
79
|
+
p = Person.new
|
80
|
+
p.initialize_fields({}) #@name == "default_value"
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/typed_fields.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require "typed_fields/version"
|
2
|
+
require 'bigdecimal'
|
3
|
+
|
4
|
+
module TypedFields
|
5
|
+
class ObjectType
|
6
|
+
def parse obj
|
7
|
+
obj
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class IntegerType
|
12
|
+
def parse obj
|
13
|
+
return nil if obj.nil?
|
14
|
+
obj.to_i
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class DecimalType
|
19
|
+
def parse obj
|
20
|
+
return nil if obj.nil?
|
21
|
+
BigDecimal.new(obj)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class StringType
|
26
|
+
def parse obj
|
27
|
+
return nil if obj.nil?
|
28
|
+
obj.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class BooleanType
|
33
|
+
def parse obj
|
34
|
+
return nil if obj.nil?
|
35
|
+
obj == "true"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ArrayType
|
40
|
+
def initialize type
|
41
|
+
@type = type
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse obj
|
45
|
+
return nil if obj.nil?
|
46
|
+
obj.map{|o| @type.parse o}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
{ :object => ObjectType.new,
|
52
|
+
:integer => IntegerType.new,
|
53
|
+
:decimal => DecimalType.new,
|
54
|
+
:string => StringType.new,
|
55
|
+
:boolean => BooleanType.new,
|
56
|
+
:custom => nil}.each do |method_name, type|
|
57
|
+
|
58
|
+
define_method method_name do |*params|
|
59
|
+
declare_fields params, type
|
60
|
+
end
|
61
|
+
|
62
|
+
array_method = "array_of_#{method_name}s"
|
63
|
+
define_method array_method do |*params|
|
64
|
+
declare_fields params, ArrayType.new(type)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def saved_type_info
|
69
|
+
name = :@@fields_type_information
|
70
|
+
if class_variable_defined?(name)
|
71
|
+
class_variable_get(name)
|
72
|
+
else
|
73
|
+
class_variable_set(name, {})
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def declare_fields params, type
|
79
|
+
options = params.last.is_a?(Hash) ? params.pop : {}
|
80
|
+
options[:type] ||= type
|
81
|
+
|
82
|
+
params.each do |field_name|
|
83
|
+
saved_type_info[field_name] = options
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module InstanceMethods
|
89
|
+
def initialize_fields params
|
90
|
+
set_default_values
|
91
|
+
params.each do |field_name, value|
|
92
|
+
set_value field_name, value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def set_value field_name, value
|
99
|
+
type_info = self.class.saved_type_info[field_name]
|
100
|
+
return unless type_info
|
101
|
+
type = type_info[:type]
|
102
|
+
parsed_value = type.parse(value)
|
103
|
+
instance_variable_set "@#{field_name.to_s}", parsed_value
|
104
|
+
end
|
105
|
+
|
106
|
+
def set_default_values
|
107
|
+
self.class.saved_type_info.each do |field_name, options|
|
108
|
+
default_value = options[:default]
|
109
|
+
default_value = default_value.call(self) if default_value.respond_to? :call
|
110
|
+
set_value field_name, default_value
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.included clazz
|
116
|
+
clazz.send :include, InstanceMethods
|
117
|
+
clazz.extend ClassMethods
|
118
|
+
end
|
119
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/typed_fields')
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe TypedFields do
|
4
|
+
|
5
|
+
let(:clazz) do
|
6
|
+
clazz = Class.new
|
7
|
+
clazz.send :include, TypedFields
|
8
|
+
clazz
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:object) do
|
12
|
+
clazz.new
|
13
|
+
end
|
14
|
+
|
15
|
+
context "basic" do
|
16
|
+
it "should declare an object field" do
|
17
|
+
clazz.object :field
|
18
|
+
object.initialize_fields :field => "value"
|
19
|
+
|
20
|
+
f(:field).should == "value"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should declare many object fields" do
|
24
|
+
clazz.object :field1, :field2
|
25
|
+
object.initialize_fields :field1 => "value1", :field2 => "value2"
|
26
|
+
|
27
|
+
f(:field1).should == "value1"
|
28
|
+
f(:field2).should == "value2"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should ignore fields that were not declared" do
|
32
|
+
clazz.object :field
|
33
|
+
object.initialize_fields :another_field => "value"
|
34
|
+
f(:another_field).should be_nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "default values" do
|
39
|
+
it "should be used when field values are not passed" do
|
40
|
+
clazz.object :field, :default => "default"
|
41
|
+
object.initialize_fields({})
|
42
|
+
f(:field).should == "default"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should not use default values specified for other fields" do
|
46
|
+
clazz.object :field1, :default => "default"
|
47
|
+
clazz.object :field2
|
48
|
+
object.initialize_fields({})
|
49
|
+
f(:field1).should == "default"
|
50
|
+
f(:field2).should be_nil
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should call default value when it is a block" do
|
54
|
+
clazz.object :field, :default => Proc.new {"default"}
|
55
|
+
object.initialize_fields({})
|
56
|
+
f(:field).should == "default"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should pass the object being constructed to a default block" do
|
60
|
+
default = double("default proc")
|
61
|
+
clazz.object :field, :default => default
|
62
|
+
|
63
|
+
default.stub(:respond_to?){true}
|
64
|
+
default.should_receive(:call).with(object).and_return("default")
|
65
|
+
|
66
|
+
object.initialize_fields({})
|
67
|
+
f(:field).should == "default"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "types" do
|
72
|
+
it "should parse integers" do
|
73
|
+
clazz.integer :field
|
74
|
+
object.initialize_fields(:field => "10")
|
75
|
+
f(:field).should == 10
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should parse decimals" do
|
79
|
+
clazz.decimal :field
|
80
|
+
object.initialize_fields(:field => "1.5")
|
81
|
+
f(:field).should == 1.5
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should parse strings" do
|
85
|
+
clazz.string :field
|
86
|
+
object.initialize_fields(:field => 100)
|
87
|
+
f(:field).should == "100"
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should parse booleans" do
|
91
|
+
clazz.boolean :field
|
92
|
+
object.initialize_fields(:field => "true")
|
93
|
+
f(:field).should == true
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should parse an array of integers" do
|
97
|
+
clazz.array_of_integers :field
|
98
|
+
object.initialize_fields(:field => ["1", "2"])
|
99
|
+
f(:field).should == [1,2]
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should use custom type" do
|
103
|
+
type = stub(:parse => "parsed")
|
104
|
+
clazz.custom :field, :type => type
|
105
|
+
object.initialize_fields(:field => nil)
|
106
|
+
f(:field).should == "parsed"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "inheritance" do
|
111
|
+
let :object do
|
112
|
+
Child.new
|
113
|
+
end
|
114
|
+
|
115
|
+
class Parent
|
116
|
+
include TypedFields
|
117
|
+
object :parent_field, :default => 'parent'
|
118
|
+
end
|
119
|
+
|
120
|
+
class Child < Parent
|
121
|
+
include TypedFields
|
122
|
+
object :child_field, :default => 'child'
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should initialize both parent and child fields" do
|
126
|
+
object.initialize_fields({})
|
127
|
+
f(:parent_field).should == "parent"
|
128
|
+
f(:child_field).should == "child"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def f field_name
|
135
|
+
object.instance_variable_get "@#{field_name.to_s}"
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "typed_fields/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "typed_fields"
|
7
|
+
s.version = TypedFields::VERSION
|
8
|
+
s.authors = ["Victor Savkin"]
|
9
|
+
s.email = ["vic.savkin@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{It types all your fields. Adds methods to convert a hash of strings into specified types.}
|
12
|
+
s.description = %q{}
|
13
|
+
|
14
|
+
s.rubyforge_project = "typed_fields"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typed_fields
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Victor Savkin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-25 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &2156932540 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2156932540
|
25
|
+
description: ''
|
26
|
+
email:
|
27
|
+
- vic.savkin@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- Gemfile
|
34
|
+
- README.md
|
35
|
+
- Rakefile
|
36
|
+
- lib/typed_fields.rb
|
37
|
+
- lib/typed_fields/version.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
- spec/typed_fields_spec.rb
|
40
|
+
- typed_fields.gemspec
|
41
|
+
homepage: ''
|
42
|
+
licenses: []
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project: typed_fields
|
61
|
+
rubygems_version: 1.8.10
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: It types all your fields. Adds methods to convert a hash of strings into
|
65
|
+
specified types.
|
66
|
+
test_files:
|
67
|
+
- spec/spec_helper.rb
|
68
|
+
- spec/typed_fields_spec.rb
|