typed_fields 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in typed_fields.gemspec
4
+ gemspec
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"
@@ -0,0 +1,3 @@
1
+ module TypedFields
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -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