id 0.0.12 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -3
  3. data/Gemfile.lock +25 -10
  4. data/LICENSE.md +1 -1
  5. data/README.md +173 -35
  6. data/id.gemspec +8 -3
  7. data/lib/id.rb +21 -15
  8. data/lib/id/active_model.rb +30 -0
  9. data/lib/id/association.rb +26 -0
  10. data/lib/id/boolean.rb +8 -0
  11. data/lib/id/coercion.rb +38 -0
  12. data/lib/id/eta_expansion.rb +5 -0
  13. data/lib/id/field.rb +46 -0
  14. data/lib/id/field/definition.rb +44 -0
  15. data/lib/id/field/summary.rb +35 -0
  16. data/lib/id/form.rb +41 -13
  17. data/lib/id/form_backwards_compatibility.rb +6 -0
  18. data/lib/id/hashifier.rb +13 -24
  19. data/lib/id/model.rb +29 -25
  20. data/lib/id/timestamps.rb +15 -15
  21. data/lib/id/validations.rb +8 -0
  22. data/spec/examples/cat_spec.rb +37 -0
  23. data/spec/lib/id/active_model_spec.rb +40 -0
  24. data/spec/lib/id/association_spec.rb +73 -0
  25. data/spec/lib/id/boolean_spec.rb +38 -0
  26. data/spec/lib/id/coercion_spec.rb +53 -0
  27. data/spec/lib/id/eta_expansion_spec.rb +13 -0
  28. data/spec/lib/id/field/definition_spec.rb +37 -0
  29. data/spec/lib/id/field/summary_spec.rb +26 -0
  30. data/spec/lib/id/field_spec.rb +62 -0
  31. data/spec/lib/id/form_spec.rb +84 -0
  32. data/spec/lib/id/hashifier_spec.rb +19 -0
  33. data/spec/lib/id/model_spec.rb +30 -180
  34. data/spec/lib/id/timestamps_spec.rb +22 -20
  35. data/spec/lib/id/validations_spec.rb +18 -0
  36. data/spec/spec_helper.rb +11 -5
  37. data/spec/support/dummy_rails_form_builder.rb +9 -0
  38. metadata +84 -26
  39. data/lib/id/form/active_model_form.rb +0 -41
  40. data/lib/id/form/descriptor.rb +0 -27
  41. data/lib/id/form/field_form.rb +0 -14
  42. data/lib/id/form/field_with_form_support.rb +0 -12
  43. data/lib/id/missing_attribute_error.rb +0 -4
  44. data/lib/id/model/association.rb +0 -49
  45. data/lib/id/model/definer.rb +0 -23
  46. data/lib/id/model/descriptor.rb +0 -23
  47. data/lib/id/model/field.rb +0 -61
  48. data/lib/id/model/has_many.rb +0 -11
  49. data/lib/id/model/has_one.rb +0 -17
  50. data/lib/id/model/type_casts.rb +0 -96
  51. data/spec/lib/id/model/association_spec.rb +0 -27
  52. data/spec/lib/id/model/field_spec.rb +0 -0
  53. data/spec/lib/id/model/form_spec.rb +0 -56
  54. data/spec/lib/id/model/type_casts_spec.rb +0 -44
  55. data/spec/lib/mike_spec.rb +0 -20
@@ -0,0 +1,8 @@
1
+ module Id::Boolean
2
+ extend self
3
+
4
+ def parse(value)
5
+ ['yes', 'true', '1'].include?(value.to_s.downcase)
6
+ end
7
+
8
+ end
@@ -0,0 +1,38 @@
1
+ module Id::Coercion
2
+ extend self
3
+
4
+ def register(from, to, &coercion)
5
+ warn "Id - Overwriting existing coercion" if coercions.has_key?([from, to])
6
+ coercions[[from, to]] = coercion
7
+ end
8
+
9
+ def coerce(value, type)
10
+ return value.map { |v| coerce(v, type) } if value.is_a? Option
11
+ return (value || []).map { |v| coerce(v, type.first) } if type.is_a? Array
12
+ return value if value.is_a? type
13
+ return type.new(value) if type.include? Id::Model
14
+
15
+ coercion = coercions.fetch([value.class, type], false)
16
+ fail Id::CoercionError, [value.class, type] unless coercion
17
+ coercion.call(value)
18
+ end
19
+
20
+ private
21
+
22
+ def coercions
23
+ @coercions ||= {}
24
+ end
25
+
26
+ end
27
+
28
+ class Id::CoercionError < StandardError
29
+ def initialize((from, to))
30
+ super "No available coercion from #{from} to #{to}"
31
+ end
32
+ end
33
+
34
+ Id::Coercion.register String, Integer, &:to_i
35
+ Id::Coercion.register String, Float, &:to_f
36
+ Id::Coercion.register String, Date, &Date.method(:parse)
37
+ Id::Coercion.register String, Time, &Time.method(:parse)
38
+ Id::Coercion.register String, Id::Boolean, &Id::Boolean.method(:parse)
@@ -0,0 +1,5 @@
1
+ module Id::EtaExpansion
2
+ def to_proc
3
+ -> data { new data }
4
+ end
5
+ end
@@ -0,0 +1,46 @@
1
+ module Id::Field
2
+
3
+ def field(name, options = {})
4
+ definition = Definition.new(name, options)
5
+ define_field!(definition)
6
+ define_predicate!(definition)
7
+ fields[name] = definition
8
+ end
9
+
10
+ def fields
11
+ @fields ||= {}
12
+ end
13
+
14
+ private
15
+
16
+ def self.extended(base)
17
+ base.send(:define_method, :fields) { self.class.fields }
18
+ end
19
+
20
+ def define_field!(definition)
21
+ send :define_method, definition.name do
22
+ _data[definition.key] or fail Id::MissingAttributeError, [self, definition]
23
+ end
24
+ end
25
+
26
+ def define_predicate!(definition)
27
+ send :define_method, "#{definition.name}?" do
28
+ value = _data[definition.key]
29
+ value && !value.is_a?(None)
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ class Id::MissingAttributeError < StandardError
36
+ def initialize((model, field))
37
+ super "#{model.class.name} had a nil value for '#{field.name}'.\n\n" +
38
+ "*** Field information ***\n#{field.to_s}\n\n" +
39
+ "*** Model data ***\n#{model.data.inspect}\n\n" +
40
+ "If you're trying to use an Id::Model in a Rails form, make sure:\n" +
41
+ "* You 'include Id::Form' in your model\n" +
42
+ "* You have the following line in your 'config/application.rb': \n\n" +
43
+ " config.action_view.default_form_builder = Id::FormBuilder\n\n"
44
+
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ module Id::Field
2
+ class Definition
3
+ def initialize(name, options)
4
+ @name = name
5
+ @options = options
6
+ end
7
+
8
+ def default
9
+ options.fetch(:default, nil)
10
+ end
11
+
12
+ def default!
13
+ default.is_a?(Proc) ? default.call : default
14
+ end
15
+
16
+ def key
17
+ options.fetch(:key, name).to_s
18
+ end
19
+
20
+ def type
21
+ options.fetch(:type, Object)
22
+ end
23
+
24
+ def optional?
25
+ options.fetch(:optional, false)
26
+ end
27
+
28
+ def to_s
29
+ Id::Field::Summary.new(self).to_s
30
+ end
31
+
32
+ def value(data)
33
+ # the following code is a bit verbose but can't use || as false is valid here
34
+ value = data[key]
35
+ value = data[key.to_sym] if value.nil?
36
+ value = default! if value.nil?
37
+ value = Option[value] if optional?
38
+
39
+ Id::Coercion.coerce(value, type) unless value.nil?
40
+ end
41
+
42
+ attr_reader :name, :options
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module Id::Field
2
+ class Summary
3
+ def initialize(definition)
4
+ @definition = definition
5
+ end
6
+
7
+ def to_s
8
+ [name, type, key, optional, default].compact.join("\n")
9
+ end
10
+
11
+ private
12
+ attr_reader :definition
13
+
14
+ def name
15
+ "Name: #{definition.name}"
16
+ end
17
+
18
+ def type
19
+ "Type: #{definition.type}" unless definition.type == Object
20
+ end
21
+
22
+ def key
23
+ "Key in hash: #{definition.key}" unless definition.key == definition.name.to_s
24
+ end
25
+
26
+ def optional
27
+ "Optional: true" if definition.optional?
28
+ end
29
+
30
+ def default
31
+ default = definition.default
32
+ "Default: #{default.is_a?(Proc) ? 'Lambda' : default}" unless default.nil?
33
+ end
34
+ end
35
+ end
@@ -1,21 +1,49 @@
1
- module Id
2
- module Form
1
+ module Id::Form
3
2
 
4
- def as_form
5
- @form_object ||= self.class.form_object.new(self)
6
- end
3
+ def active_model
4
+ @active_model ||= form_class.new(self, _data)
5
+ end
7
6
 
8
- def errors
9
- as_form.errors
10
- end
7
+ def as_form
8
+ warn '[DEPRECATION] calling `as_form` is deprecated, please use `to_model` instead'
9
+ to_model
10
+ end
11
11
 
12
- def valid?
13
- as_form.valid?
14
- end
12
+ def persisted?
13
+ false
14
+ end
15
+
16
+ delegate :valid?, :errors, to: :active_model
15
17
 
16
- def self.included(base)
17
- base.extend(Descriptor)
18
+ private
19
+ def form_class
20
+ self.class.form_class
21
+ end
22
+
23
+ def self.included(base)
24
+ base.send :include, ActiveModel::Conversion
25
+ base.send :extend, Id::Validations, Id::FormBackwardsCompatibility
26
+ base.send :extend, ActiveModel::Naming
27
+ base.send :alias_method, :to_model, :active_model
28
+
29
+ base.define_singleton_method :form_class do
30
+ base = self
31
+ @form_class ||= Class.new(Id::ActiveModel) do
32
+ eigenclass = class << self; self end
33
+ eigenclass.send :define_method, :model_name do
34
+ base.send(:model_name)
35
+ end
36
+ end
18
37
  end
38
+ end
19
39
 
40
+ end
41
+
42
+ if defined?(ActionView::Helpers::FormBuilder)
43
+ class Id::FormBuilder < ActionView::Helpers::FormBuilder
44
+ def initialize(object_name, object, template, options)
45
+ object = object.is_a?(Id::Model) ? object.to_model : object
46
+ super object_name, object, template, options
47
+ end
20
48
  end
21
49
  end
@@ -0,0 +1,6 @@
1
+ module Id::FormBackwardsCompatibility
2
+ def form(&block)
3
+ warn '[DEPRECATION] form is no longer needed - validation can be specified at the top level of the model'
4
+ form_class.send :instance_exec, &block
5
+ end
6
+ end
@@ -1,28 +1,17 @@
1
- module Id
2
- class Hashifier
1
+ module Id::Hashifier
2
+ extend self
3
3
 
4
- def self.hashify(data)
5
- new(data).hashify
4
+ def enhash(value)
5
+ case value
6
+ when Hash
7
+ value.reduce({}) { |acc, (k, v)| acc.merge(k.to_s => enhash(v)) }
8
+ when Array
9
+ value.map { |v| enhash(v) }
10
+ when Id::Model
11
+ value.to_hash
12
+ else
13
+ value
6
14
  end
7
-
8
- def initialize(data)
9
- @data = data
10
- end
11
-
12
- def hashify
13
- Hash[data.map { |k, v| [ k.to_s, as_data(v) ] }]
14
- end
15
-
16
- private
17
-
18
- def as_data(v)
19
- case v
20
- when Id::Model then v.data
21
- when Array then v.first.is_a?(Id::Model) ? v.map(&:data) : v
22
- when Hash then Hashifier.hashify(v)
23
- else v end
24
- end
25
-
26
- attr_reader :data
27
15
  end
16
+
28
17
  end
@@ -1,37 +1,41 @@
1
- module Id
2
- module Model
3
- attr_reader :data
1
+ module Id::Model
4
2
 
5
- def initialize(data = {})
6
- @data = Hashifier.hashify(data)
3
+ def initialize(_data = {})
4
+ @_data = fields.reduce({}) do |acc, (_, field)|
5
+ acc.merge(field.key => field.value(_data))
7
6
  end
7
+ end
8
8
 
9
- def set(values)
10
- self.class.new(data.merge(Hashifier.hashify(values)))
11
- end
9
+ def set(update)
10
+ updated = _data.merge(update.stringify_keys)
11
+ self.class.new(updated)
12
+ end
12
13
 
13
- def unset(*keys)
14
- self.class.new(data.except(*keys.map(&:to_s)))
15
- end
14
+ def unset(*fields)
15
+ fields = fields.map(&:to_s)
16
+ updated = _data.except(*fields)
17
+ self.class.new(updated)
18
+ end
16
19
 
17
- def eql? other
18
- other.is_a?(Id::Model) && other.data.eql?(self.data)
19
- end
20
- alias_method :==, :eql?
20
+ def eql? other
21
+ other.is_a?(Id::Model) && other._data.eql?(self._data)
22
+ end
23
+ alias_method :==, :eql?
21
24
 
22
- def hash
23
- data.hash
24
- end
25
+ def hash
26
+ _data.hash
27
+ end
25
28
 
26
- private
29
+ def data
30
+ @data ||= Id::Hashifier.enhash(_data)
31
+ end
27
32
 
28
- def self.included(base)
29
- base.extend(Descriptor)
30
- end
33
+ alias_method :to_hash, :data
31
34
 
32
- def memoize(f, &b)
33
- instance_variable_get("@#{f}") || instance_variable_set("@#{f}", b.call(data))
34
- end
35
+ protected
36
+ attr_reader :_data
35
37
 
38
+ def self.included(base)
39
+ base.send :extend, Id::Field, Id::Association, Id::EtaExpansion
36
40
  end
37
41
  end
@@ -1,20 +1,20 @@
1
- module Id
2
- module Timestamps
3
- def self.included(base)
4
- base.field :created_at
5
- base.field :updated_at
6
- end
1
+ module Id::Timestamps
2
+ def self.included(base)
3
+ base.field :created_at
4
+ base.field :updated_at
5
+ end
7
6
 
8
- def initialize(data = {})
9
- super data.merge(:created_at => data.fetch('created_at', Time.now))
10
- end
7
+ def initialize(_data = {})
8
+ now = Time.now
9
+ super ({ created_at: now, updated_at: now }).merge(_data)
10
+ end
11
11
 
12
- def set(values)
13
- self.class.new(super.data.merge(:updated_at => Time.now))
14
- end
12
+ def set(update)
13
+ super update.merge(updated_at: Time.now)
14
+ end
15
15
 
16
- def unset(*keys)
17
- self.class.new(super.data.merge(:updated_at => Time.now))
18
- end
16
+ def unset(update)
17
+ super.set({})
19
18
  end
19
+
20
20
  end
@@ -0,0 +1,8 @@
1
+ module Id::Validations
2
+ delegate :validate, to: :form_class
3
+ delegate :validates, to: :form_class
4
+ delegate :validates!, to: :form_class
5
+ delegate :validates_each, to: :form_class
6
+ delegate :validates_with, to: :form_class
7
+ delegate :validates_presence_of, to: :form_class
8
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ class Mouse; include Id::Model end
4
+
5
+ class Cat
6
+ include Id::Model
7
+
8
+ field :name
9
+ field :paws, type: Integer
10
+ field :mice, type: Array[Mouse]
11
+ field :friendly, type: Id::Boolean
12
+ end
13
+
14
+ describe Cat do
15
+ it 'has a name' do
16
+ cat = Cat.new(name: 'Tracy')
17
+ expect(cat.name).to eq 'Tracy'
18
+ end
19
+
20
+ it 'has an integer number of paws' do
21
+ cat = Cat.new(paws: '3')
22
+ expect(cat.paws).to eq 3
23
+ end
24
+
25
+ it 'has an array of mice' do
26
+ cat = Cat.new(mice: [{},{},{}])
27
+ expect(cat.mice).to have(3).items
28
+ cat.mice.each { |mouse| expect(mouse).to be_a Mouse }
29
+ end
30
+
31
+ it 'is either friendly or unfriendly' do
32
+ cat = Cat.new(friendly: 'yes')
33
+ expect(cat).to be_friendly
34
+ cat = Cat.new(friendly: 'nope')
35
+ expect(cat).not_to be_friendly
36
+ end
37
+ end