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 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