typed_fields 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|