id 0.0.12 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|