id 0.0.12 → 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.
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