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.
- checksums.yaml +4 -4
- data/Gemfile +0 -3
- data/Gemfile.lock +25 -10
- data/LICENSE.md +1 -1
- data/README.md +173 -35
- data/id.gemspec +8 -3
- data/lib/id.rb +21 -15
- data/lib/id/active_model.rb +30 -0
- data/lib/id/association.rb +26 -0
- data/lib/id/boolean.rb +8 -0
- data/lib/id/coercion.rb +38 -0
- data/lib/id/eta_expansion.rb +5 -0
- data/lib/id/field.rb +46 -0
- data/lib/id/field/definition.rb +44 -0
- data/lib/id/field/summary.rb +35 -0
- data/lib/id/form.rb +41 -13
- data/lib/id/form_backwards_compatibility.rb +6 -0
- data/lib/id/hashifier.rb +13 -24
- data/lib/id/model.rb +29 -25
- data/lib/id/timestamps.rb +15 -15
- data/lib/id/validations.rb +8 -0
- data/spec/examples/cat_spec.rb +37 -0
- data/spec/lib/id/active_model_spec.rb +40 -0
- data/spec/lib/id/association_spec.rb +73 -0
- data/spec/lib/id/boolean_spec.rb +38 -0
- data/spec/lib/id/coercion_spec.rb +53 -0
- data/spec/lib/id/eta_expansion_spec.rb +13 -0
- data/spec/lib/id/field/definition_spec.rb +37 -0
- data/spec/lib/id/field/summary_spec.rb +26 -0
- data/spec/lib/id/field_spec.rb +62 -0
- data/spec/lib/id/form_spec.rb +84 -0
- data/spec/lib/id/hashifier_spec.rb +19 -0
- data/spec/lib/id/model_spec.rb +30 -180
- data/spec/lib/id/timestamps_spec.rb +22 -20
- data/spec/lib/id/validations_spec.rb +18 -0
- data/spec/spec_helper.rb +11 -5
- data/spec/support/dummy_rails_form_builder.rb +9 -0
- metadata +84 -26
- data/lib/id/form/active_model_form.rb +0 -41
- data/lib/id/form/descriptor.rb +0 -27
- data/lib/id/form/field_form.rb +0 -14
- data/lib/id/form/field_with_form_support.rb +0 -12
- data/lib/id/missing_attribute_error.rb +0 -4
- data/lib/id/model/association.rb +0 -49
- data/lib/id/model/definer.rb +0 -23
- data/lib/id/model/descriptor.rb +0 -23
- data/lib/id/model/field.rb +0 -61
- data/lib/id/model/has_many.rb +0 -11
- data/lib/id/model/has_one.rb +0 -17
- data/lib/id/model/type_casts.rb +0 -96
- data/spec/lib/id/model/association_spec.rb +0 -27
- data/spec/lib/id/model/field_spec.rb +0 -0
- data/spec/lib/id/model/form_spec.rb +0 -56
- data/spec/lib/id/model/type_casts_spec.rb +0 -44
- data/spec/lib/mike_spec.rb +0 -20
data/lib/id/boolean.rb
ADDED
data/lib/id/coercion.rb
ADDED
@@ -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)
|
data/lib/id/field.rb
ADDED
@@ -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
|
data/lib/id/form.rb
CHANGED
@@ -1,21 +1,49 @@
|
|
1
|
-
module Id
|
2
|
-
module Form
|
1
|
+
module Id::Form
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
def active_model
|
4
|
+
@active_model ||= form_class.new(self, _data)
|
5
|
+
end
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
12
|
+
def persisted?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
delegate :valid?, :errors, to: :active_model
|
15
17
|
|
16
|
-
|
17
|
-
|
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
|
data/lib/id/hashifier.rb
CHANGED
@@ -1,28 +1,17 @@
|
|
1
|
-
module Id
|
2
|
-
|
1
|
+
module Id::Hashifier
|
2
|
+
extend self
|
3
3
|
|
4
|
-
|
5
|
-
|
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
|
data/lib/id/model.rb
CHANGED
@@ -1,37 +1,41 @@
|
|
1
|
-
module Id
|
2
|
-
module Model
|
3
|
-
attr_reader :data
|
1
|
+
module Id::Model
|
4
2
|
|
5
|
-
|
6
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
9
|
+
def set(update)
|
10
|
+
updated = _data.merge(update.stringify_keys)
|
11
|
+
self.class.new(updated)
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
def eql? other
|
21
|
+
other.is_a?(Id::Model) && other._data.eql?(self._data)
|
22
|
+
end
|
23
|
+
alias_method :==, :eql?
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
def hash
|
26
|
+
_data.hash
|
27
|
+
end
|
25
28
|
|
26
|
-
|
29
|
+
def data
|
30
|
+
@data ||= Id::Hashifier.enhash(_data)
|
31
|
+
end
|
27
32
|
|
28
|
-
|
29
|
-
base.extend(Descriptor)
|
30
|
-
end
|
33
|
+
alias_method :to_hash, :data
|
31
34
|
|
32
|
-
|
33
|
-
|
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
|
data/lib/id/timestamps.rb
CHANGED
@@ -1,20 +1,20 @@
|
|
1
|
-
module Id
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def initialize(_data = {})
|
8
|
+
now = Time.now
|
9
|
+
super ({ created_at: now, updated_at: now }).merge(_data)
|
10
|
+
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
def set(update)
|
13
|
+
super update.merge(updated_at: Time.now)
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
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
|