structural 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.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CONTRIBUTING.md +13 -0
- data/Gemfile +10 -0
- data/LICENSE.md +22 -0
- data/README.md +55 -0
- data/Rakefile +29 -0
- data/lib/structural.rb +15 -0
- data/lib/structural/hashifier.rb +27 -0
- data/lib/structural/missing_attribute_error.rb +4 -0
- data/lib/structural/model.rb +36 -0
- data/lib/structural/model/association.rb +47 -0
- data/lib/structural/model/definer.rb +21 -0
- data/lib/structural/model/descriptor.rb +21 -0
- data/lib/structural/model/field.rb +57 -0
- data/lib/structural/model/has_many.rb +9 -0
- data/lib/structural/model/has_one.rb +10 -0
- data/lib/structural/model/type_casts.rb +89 -0
- data/lib/structural/timestamps.rb +20 -0
- data/lib/structural/version.rb +3 -0
- data/spec/lib/structural/model/association_spec.rb +26 -0
- data/spec/lib/structural/model/field_spec.rb +0 -0
- data/spec/lib/structural/model/type_casts_spec.rb +42 -0
- data/spec/lib/structural/model_spec.rb +159 -0
- data/spec/lib/structural/timestamps_spec.rb +29 -0
- data/spec/spec_helper.rb +12 -0
- data/structural.gemspec +30 -0
- metadata +193 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ea9e0a40c502402274176c463486b3ad8f40dbbd
|
4
|
+
data.tar.gz: da085f4a8a31ccbd7a031f198accfdcf7aa20351
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 15710f21370fcfb50d32b7f90b756534a41ab215c021083876d6ed8fb0698ba879a42cfd47549c0500091389efa4fd2850a956863a6f88db121ce52a205af40b
|
7
|
+
data.tar.gz: 2b386c4d70892fe5f6b699c32e9752c18a1b23b5ec2caaf6ed156cae29cc346269d0e309691982c9596715be1c4a4d2d26620b0e877401cb23bd51ea77645157
|
data/.gitignore
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
tags
|
24
|
+
coverage
|
25
|
+
.rspec
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ree-1.8.7-2012.02
|
data/.travis.yml
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
## Contributing
|
2
|
+
|
3
|
+
We love to get pull requests, here's how we like to work:
|
4
|
+
|
5
|
+
- Open a pull request. Before you start coding, let's make sure we're on the same page.
|
6
|
+
- Fork the repo.
|
7
|
+
- Run the tests. We only take pull requests with passing tests: `bundle && rake`
|
8
|
+
- Add a test for your change. Only refactoring and documentation changes require no new tests.
|
9
|
+
- Make the test pass, following the conventions you see used in the source already.
|
10
|
+
- If you have made a number of commits, squash them before pushing with.
|
11
|
+
- Push to your fork and update your pull request.
|
12
|
+
- We may suggest some changes or improvements or alternatives.
|
13
|
+
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 On The Beach Ltd
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Structural
|
2
|
+
|
3
|
+
`Structural` is a cut down fork of `Id`, designed to work with `Ruby >= 1.8.7` and
|
4
|
+
`Rails >= 2.3`.
|
5
|
+
|
6
|
+
#### Defining a model
|
7
|
+
|
8
|
+
class MyModel
|
9
|
+
include Structural::Model
|
10
|
+
|
11
|
+
field :foo
|
12
|
+
field :bar, default: 42
|
13
|
+
field :baz, key: 'barry'
|
14
|
+
end
|
15
|
+
|
16
|
+
my_model = MyModel.new(foo: 7, barry: 'hello')
|
17
|
+
my_model.foo # => 7
|
18
|
+
my_model.bar # => 42
|
19
|
+
my_model.baz # => 'hello'
|
20
|
+
|
21
|
+
Default values can be specified, as well as key aliases.
|
22
|
+
|
23
|
+
#### Associations
|
24
|
+
|
25
|
+
We can also specify `has_one` or `has_many` associations for nested documents:
|
26
|
+
|
27
|
+
class Zoo
|
28
|
+
include Structural::Model
|
29
|
+
|
30
|
+
has_many :lions
|
31
|
+
has_many :zebras
|
32
|
+
has_one :zookeeper, type: Person
|
33
|
+
end
|
34
|
+
|
35
|
+
zoo = Zoo.new(lions: [{name: 'Hetty'}],
|
36
|
+
zebras: [{name: 'Lisa'}],
|
37
|
+
zookeeper: {name: 'Russell'})
|
38
|
+
|
39
|
+
zoo.lions.first.class # => Lion
|
40
|
+
zoo.lions.first.name # => "Hetty"
|
41
|
+
zoo.zookeeper.class # => Person
|
42
|
+
zoo.zookeeper.name # => "Russell"
|
43
|
+
|
44
|
+
Types are inferred from the association name unless one is specified.
|
45
|
+
|
46
|
+
#### Designed for immutability
|
47
|
+
|
48
|
+
`structural` models provide accessor methods, but no mutator methods.
|
49
|
+
When changing the value of a field, a new copy of the object is created:
|
50
|
+
|
51
|
+
person = Person.new(name: 'Russell', job: 'programmer')
|
52
|
+
person.set(name: 'Radek')
|
53
|
+
# => returns a new Person whose name is Radek and whose job is 'programmer'
|
54
|
+
person.hat.set(colour: 'red')
|
55
|
+
# => returns a new person object with a new hat object with its colour set to red
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
# encoding: utf-8
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'bundler'
|
7
|
+
begin
|
8
|
+
Bundler.setup(:default, :development)
|
9
|
+
rescue Bundler::BundlerError => e
|
10
|
+
$stderr.puts e.message
|
11
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
12
|
+
exit e.status_code
|
13
|
+
end
|
14
|
+
require 'rake'
|
15
|
+
|
16
|
+
require 'rspec/core'
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
19
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
20
|
+
end
|
21
|
+
|
22
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
23
|
+
ENV['COVERAGE'] = 'coverage'
|
24
|
+
end
|
25
|
+
|
26
|
+
task :default => :spec
|
27
|
+
|
28
|
+
require 'yard'
|
29
|
+
YARD::Rake::YardocTask.new
|
data/lib/structural.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'money'
|
3
|
+
|
4
|
+
require 'structural/version'
|
5
|
+
require 'structural/missing_attribute_error'
|
6
|
+
require 'structural/hashifier'
|
7
|
+
require 'structural/model/definer'
|
8
|
+
require 'structural/model/descriptor'
|
9
|
+
require 'structural/model/type_casts'
|
10
|
+
require 'structural/model/field'
|
11
|
+
require 'structural/model/association'
|
12
|
+
require 'structural/model/has_one'
|
13
|
+
require 'structural/model/has_many'
|
14
|
+
require 'structural/model'
|
15
|
+
require 'structural/timestamps'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Structural
|
2
|
+
class Hashifier
|
3
|
+
def self.hashify(data)
|
4
|
+
new(data).hashify
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
@data = data
|
9
|
+
end
|
10
|
+
|
11
|
+
def hashify
|
12
|
+
Hash[data.map { |k, v| [ k.to_s, as_data(v) ] }]
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def as_data(v)
|
18
|
+
case v
|
19
|
+
when Structural::Model then v.data
|
20
|
+
when Array then v.first.is_a?(Structural::Model) ? v.map(&:data) : v
|
21
|
+
when Hash then Hashifier.hashify(v)
|
22
|
+
else v end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :data
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Structural
|
2
|
+
module Model
|
3
|
+
attr_reader :data
|
4
|
+
|
5
|
+
def initialize(data = {})
|
6
|
+
@data = Hashifier.hashify(data)
|
7
|
+
end
|
8
|
+
|
9
|
+
def set(values)
|
10
|
+
self.class.new(data.merge(Hashifier.hashify(values)))
|
11
|
+
end
|
12
|
+
|
13
|
+
def unset(*keys)
|
14
|
+
self.class.new(data.except(*keys.map(&:to_s)))
|
15
|
+
end
|
16
|
+
|
17
|
+
def eql? other
|
18
|
+
other.is_a?(Structural::Model) && other.data.eql?(self.data)
|
19
|
+
end
|
20
|
+
alias_method :==, :eql?
|
21
|
+
|
22
|
+
def hash
|
23
|
+
data.hash
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def self.included(base)
|
29
|
+
base.extend(Descriptor)
|
30
|
+
end
|
31
|
+
|
32
|
+
def memoize(f, &b)
|
33
|
+
instance_variable_get("@#{f}") || instance_variable_set("@#{f}", b.call(data))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Structural
|
2
|
+
module Model
|
3
|
+
class Association < Field
|
4
|
+
def type
|
5
|
+
options.fetch(:type) { inferred_class }
|
6
|
+
end
|
7
|
+
|
8
|
+
def inferred_class
|
9
|
+
hierarchy.parent.const_get(inferred_class_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def inferred_class_name
|
13
|
+
@inferred_class_name ||= name.to_s.classify
|
14
|
+
end
|
15
|
+
|
16
|
+
def hierarchy
|
17
|
+
@hierarchy ||= Hierarchy.new(model.name, inferred_class_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
class Hierarchy
|
21
|
+
def initialize(path, child)
|
22
|
+
@path = path
|
23
|
+
@child = child
|
24
|
+
end
|
25
|
+
|
26
|
+
def parent
|
27
|
+
@parent ||= constants.find do |c|
|
28
|
+
c.ancestors.find { |anc| anc.const_defined? child }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def constants
|
33
|
+
hierarchy.map(&:constantize)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def hierarchy(name=path)
|
39
|
+
name.match(/(.*)::.*$/)
|
40
|
+
$1 ? [name] + hierarchy($1) : [name]
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :path, :child
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Structural
|
2
|
+
module Model
|
3
|
+
class Definer
|
4
|
+
def self.method_memoize(context, name, &value_block)
|
5
|
+
method(context, name) do |object|
|
6
|
+
object.instance_eval do
|
7
|
+
memoize(name, &value_block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.method(context, name, &value_block)
|
13
|
+
context.instance_eval do
|
14
|
+
define_method name do
|
15
|
+
value_block.call(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Structural
|
2
|
+
module Model
|
3
|
+
module Descriptor
|
4
|
+
def field(f, options={})
|
5
|
+
Field.new(self, f, options).define
|
6
|
+
end
|
7
|
+
|
8
|
+
def has_one(f, options={})
|
9
|
+
HasOne.new(self, f, options).define
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_many(f, options={})
|
13
|
+
HasMany.new(self, f, options).define
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_proc
|
17
|
+
lambda { |data| new data }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Structural
|
2
|
+
module Model
|
3
|
+
class Field
|
4
|
+
def initialize(model, name, options)
|
5
|
+
@model = model
|
6
|
+
@name = name
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def define
|
11
|
+
Definer.method_memoize(model, name) { |data| value_of(data) }
|
12
|
+
Definer.method(model, "#{name}?") { |obj| presence_of(obj.data) }
|
13
|
+
hook_define
|
14
|
+
end
|
15
|
+
|
16
|
+
def hook_define
|
17
|
+
end
|
18
|
+
|
19
|
+
def value_of(data)
|
20
|
+
value = data.fetch(key, &default_value)
|
21
|
+
cast(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def presence_of(data)
|
25
|
+
data.has_key?(key) && !data.fetch(key).nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def cast(value)
|
29
|
+
TypeCasts.cast(options.fetch(:type, false), value)
|
30
|
+
end
|
31
|
+
|
32
|
+
def key
|
33
|
+
options.fetch(:key, name).to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_value
|
37
|
+
proc do
|
38
|
+
if default?
|
39
|
+
default
|
40
|
+
else
|
41
|
+
raise MissingAttributeError, key
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def default?
|
47
|
+
options.has_key?(:default)
|
48
|
+
end
|
49
|
+
|
50
|
+
def default
|
51
|
+
options.fetch(:default)
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :model, :name, :options
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Structural
|
2
|
+
module Model
|
3
|
+
module TypeCasts
|
4
|
+
def self.cast(type, value)
|
5
|
+
casts.fetch(type, Identity).new(value).cast
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def self.register(cast)
|
11
|
+
casts[cast.type] = cast
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def self.casts
|
17
|
+
@casts ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
class Cast
|
21
|
+
def initialize(value)
|
22
|
+
@value = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def cast
|
26
|
+
value.is_a?(type) ? value : conversion
|
27
|
+
end
|
28
|
+
|
29
|
+
def type
|
30
|
+
self.class.type
|
31
|
+
end
|
32
|
+
|
33
|
+
def conversion
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.type
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :value
|
44
|
+
end
|
45
|
+
|
46
|
+
class Identity < Cast
|
47
|
+
def cast
|
48
|
+
value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Date < Cast
|
53
|
+
def self.type
|
54
|
+
::Date
|
55
|
+
end
|
56
|
+
|
57
|
+
def conversion
|
58
|
+
::Date.parse value
|
59
|
+
end
|
60
|
+
|
61
|
+
TypeCasts.register(self)
|
62
|
+
end
|
63
|
+
|
64
|
+
class Time < Cast
|
65
|
+
def self.type
|
66
|
+
::Time
|
67
|
+
end
|
68
|
+
|
69
|
+
def conversion
|
70
|
+
::Time.parse value
|
71
|
+
end
|
72
|
+
|
73
|
+
TypeCasts.register(self)
|
74
|
+
end
|
75
|
+
|
76
|
+
class Money < Cast
|
77
|
+
def self.type
|
78
|
+
::Money
|
79
|
+
end
|
80
|
+
|
81
|
+
def conversion
|
82
|
+
::Money.new value.to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
TypeCasts.register(self)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Structural
|
2
|
+
module Timestamps
|
3
|
+
def self.included(base)
|
4
|
+
base.field :created_at
|
5
|
+
base.field :updated_at
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(data = {})
|
9
|
+
super data.merge(:created_at => data.fetch('created_at', Time.now))
|
10
|
+
end
|
11
|
+
|
12
|
+
def set(values)
|
13
|
+
self.class.new(super.data.merge(:updated_at => Time.now))
|
14
|
+
end
|
15
|
+
|
16
|
+
def unset(*keys)
|
17
|
+
self.class.new(super.data.merge(:updated_at => Time.now))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Structural::Model::Association do
|
4
|
+
module Foo
|
5
|
+
module Bar
|
6
|
+
module Baz
|
7
|
+
class Quux
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:model) { double(:name => "Foo::Bar::Baz::Quux") }
|
14
|
+
let(:has_one) { Structural::Model::Association.new(model, "yak", {}) }
|
15
|
+
|
16
|
+
describe "hierarchy" do
|
17
|
+
it "builds the class and module hierarchy for the model" do
|
18
|
+
has_one.hierarchy.constants.should eq [
|
19
|
+
Foo::Bar::Baz::Quux,
|
20
|
+
Foo::Bar::Baz,
|
21
|
+
Foo::Bar,
|
22
|
+
Foo
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
File without changes
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Structural
|
4
|
+
module Model
|
5
|
+
module TypeCasts
|
6
|
+
describe Cast do
|
7
|
+
it 'requires a type' do
|
8
|
+
expect { Cast.new(1).type }.to raise_error NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'requires a conversion' do
|
12
|
+
expect { Cast.new(1).conversion }.to raise_error NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe Date do
|
18
|
+
it 'casts strings to dates' do
|
19
|
+
Date.new("06-06-1983").cast.should eq ::Date.new(1983, 6, 6)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Time do
|
24
|
+
it 'casts strings to Times' do
|
25
|
+
Time.new("06-06-1983").cast.should eq ::Time.parse("06-06-1983")
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'does nothing if the value is already of the correct type' do
|
29
|
+
time = ::Time.now
|
30
|
+
Time.new(time).cast.should eq time
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Money do
|
35
|
+
it 'ints or strings to Money' do
|
36
|
+
Money.new("500").cast.should eq ::Money.new(5_00)
|
37
|
+
Money.new(500).cast.should eq ::Money.new(5_00)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class NestedModel
|
4
|
+
include Structural::Model
|
5
|
+
field :yak
|
6
|
+
end
|
7
|
+
|
8
|
+
class TestModel
|
9
|
+
include Structural::Model
|
10
|
+
|
11
|
+
field :foo
|
12
|
+
field :bar, :key => 'baz'
|
13
|
+
field :quux, :default => false
|
14
|
+
field :date_of_birth, :type => Date
|
15
|
+
field :empty_date, :type => Date
|
16
|
+
field :christmas, :default => Date.new(2014,12,25), :type => Date
|
17
|
+
|
18
|
+
has_one :aliased_model, :type => NestedModel
|
19
|
+
has_one :nested_model, :key => 'aliased_model'
|
20
|
+
has_one :extra_nested_model
|
21
|
+
has_one :test_model
|
22
|
+
has_many :nested_models
|
23
|
+
|
24
|
+
class ExtraNestedModel
|
25
|
+
include Structural::Model
|
26
|
+
field :cats
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe Structural::Model do
|
31
|
+
let(:model) do
|
32
|
+
TestModel.new(
|
33
|
+
:foo => 3,
|
34
|
+
:baz => 6,
|
35
|
+
:quxx => 8,
|
36
|
+
:test_model => {},
|
37
|
+
:date_of_birth => '06-06-1983',
|
38
|
+
:aliased_model => {'yak' => 11},
|
39
|
+
:nested_models => [{'yak' => 11}, {:yak => 14}],
|
40
|
+
:extra_nested_model => { :cats => "MIAOW" }
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
describe ".new" do
|
46
|
+
it 'converts any passed Structural models to their hash representations' do
|
47
|
+
new_model = TestModel.new(:test_model => model)
|
48
|
+
new_model.test_model.data.should eq model.data
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe ".field" do
|
53
|
+
it 'defines an accessor on the model' do
|
54
|
+
model.foo.should eq 3
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'allows key aliases' do
|
58
|
+
model.bar.should eq 6
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'allows default values' do
|
62
|
+
model.quux.should be_false
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "typecast option" do
|
66
|
+
it 'typecasts to the provided type if a cast exists' do
|
67
|
+
model.date_of_birth.should be_a Date
|
68
|
+
model.christmas.should be_a Date
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe ".has_one" do
|
74
|
+
it "allows nested models" do
|
75
|
+
model.aliased_model.should be_a NestedModel
|
76
|
+
end
|
77
|
+
it "allows nested models" do
|
78
|
+
model.nested_model.should be_a NestedModel
|
79
|
+
model.nested_model.yak.should eq 11
|
80
|
+
end
|
81
|
+
it "allows associations to be nested within the class" do
|
82
|
+
model.extra_nested_model.should be_a TestModel::ExtraNestedModel
|
83
|
+
model.extra_nested_model.cats.should eq 'MIAOW'
|
84
|
+
end
|
85
|
+
it "allows recursively defined models" do
|
86
|
+
model.test_model.should be_a TestModel
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe ".has_many" do
|
91
|
+
it 'creates an array of nested models' do
|
92
|
+
model.nested_models.should be_a Array
|
93
|
+
model.nested_models.first.should be_a NestedModel
|
94
|
+
model.nested_models.first.yak.should eq 11
|
95
|
+
model.nested_models.last.yak.should eq 14
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "#set" do
|
100
|
+
it "creates a new model with the provided values changed" do
|
101
|
+
model.set(:foo => 999).should be_a TestModel
|
102
|
+
model.set(:foo => 999).foo.should eq 999
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#unset" do
|
107
|
+
it 'returns a new basket minus the passed key' do
|
108
|
+
expect { model.set(:foo => 999, :bar => 555).unset(:foo, :bar).foo }.to raise_error Structural::MissingAttributeError, "foo"
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'does not error if the key to be removed does not exist' do
|
112
|
+
expect { model.unset(:not_in_hash) }.to_not raise_error
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#fields are present methods" do
|
117
|
+
it 'allows you to check if fields are present' do
|
118
|
+
model = TestModel.new(:foo => 1)
|
119
|
+
model.foo?.should be_true
|
120
|
+
model.bar?.should be_false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#==" do
|
125
|
+
it 'is equal to another Structural model with the same data' do
|
126
|
+
one = TestModel.new(:foo => 1)
|
127
|
+
two = TestModel.new(:foo => 1)
|
128
|
+
one.should eq two
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'is not equal to two models with different data' do
|
132
|
+
one = TestModel.new(:foo => 1)
|
133
|
+
two = TestModel.new(:foo => 2)
|
134
|
+
one.should_not eq two
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#hash" do
|
139
|
+
it 'allows Structural models to be used as hash keys' do
|
140
|
+
one = TestModel.new(:foo => 1)
|
141
|
+
two = TestModel.new(:foo => 1)
|
142
|
+
hash = { one => :found }
|
143
|
+
hash[two].should eq :found
|
144
|
+
end
|
145
|
+
it 'they are different keys if the data is different' do
|
146
|
+
one = TestModel.new(:foo => 1)
|
147
|
+
two = TestModel.new(:foo => 2)
|
148
|
+
hash = { one => :found }
|
149
|
+
hash[two].should be_nil
|
150
|
+
hash[one].should eq :found
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "#to_proc" do
|
155
|
+
it 'eta expands the model class into its constructor' do
|
156
|
+
[{},{}].map(&TestModel).all? { |m| m.is_a? TestModel }.should be_true
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TimeStampedModel
|
4
|
+
include Structural::Model
|
5
|
+
include Structural::Timestamps
|
6
|
+
|
7
|
+
field :foo
|
8
|
+
field :bar
|
9
|
+
end
|
10
|
+
|
11
|
+
module Structural
|
12
|
+
describe Timestamps do
|
13
|
+
let(:model) { TimeStampedModel.new(:foo => 999, :bar => 666) }
|
14
|
+
|
15
|
+
it 'should have a created_at date' do
|
16
|
+
model.created_at.should be_a Time
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should update the updated at when set is called' do
|
20
|
+
updated = model.set(:foo => 123)
|
21
|
+
expect(updated.created_at).to be < updated.updated_at
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should update the updated at when unset is called' do
|
25
|
+
updated = model.unset(:foo)
|
26
|
+
expect(updated.created_at).to be < updated.updated_at
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/structural.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'structural/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'structural'
|
8
|
+
spec.version = Structural::VERSION
|
9
|
+
spec.authors = ['Russell Dunphy', 'Radek Molenda', 'Jon Doveston']
|
10
|
+
spec.email = ['jon.doveston@onthebeach.co.uk']
|
11
|
+
spec.summary = %q{Simple models based on hashes}
|
12
|
+
spec.description = %q{A cut down fork of the Id gem developed at On The Beach Ltd.}
|
13
|
+
spec.homepage = 'https://github.com/onthebeach/structural'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'money'
|
22
|
+
spec.add_dependency 'activesupport'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
spec.add_development_dependency 'simplecov'
|
26
|
+
spec.add_development_dependency 'yard'
|
27
|
+
spec.add_development_dependency 'redcarpet'
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
29
|
+
spec.add_development_dependency 'rake'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: structural
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Russell Dunphy
|
8
|
+
- Radek Molenda
|
9
|
+
- Jon Doveston
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2014-05-12 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: money
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - '>='
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: activesupport
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rspec
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: simplecov
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: yard
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: redcarpet
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: bundler
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ~>
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '1.6'
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ~>
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '1.6'
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: rake
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
type: :development
|
121
|
+
prerelease: false
|
122
|
+
version_requirements: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
description: A cut down fork of the Id gem developed at On The Beach Ltd.
|
128
|
+
email:
|
129
|
+
- jon.doveston@onthebeach.co.uk
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- .gitignore
|
135
|
+
- .ruby-version
|
136
|
+
- .travis.yml
|
137
|
+
- CONTRIBUTING.md
|
138
|
+
- Gemfile
|
139
|
+
- LICENSE.md
|
140
|
+
- README.md
|
141
|
+
- Rakefile
|
142
|
+
- lib/structural.rb
|
143
|
+
- lib/structural/hashifier.rb
|
144
|
+
- lib/structural/missing_attribute_error.rb
|
145
|
+
- lib/structural/model.rb
|
146
|
+
- lib/structural/model/association.rb
|
147
|
+
- lib/structural/model/definer.rb
|
148
|
+
- lib/structural/model/descriptor.rb
|
149
|
+
- lib/structural/model/field.rb
|
150
|
+
- lib/structural/model/has_many.rb
|
151
|
+
- lib/structural/model/has_one.rb
|
152
|
+
- lib/structural/model/type_casts.rb
|
153
|
+
- lib/structural/timestamps.rb
|
154
|
+
- lib/structural/version.rb
|
155
|
+
- spec/lib/structural/model/association_spec.rb
|
156
|
+
- spec/lib/structural/model/field_spec.rb
|
157
|
+
- spec/lib/structural/model/type_casts_spec.rb
|
158
|
+
- spec/lib/structural/model_spec.rb
|
159
|
+
- spec/lib/structural/timestamps_spec.rb
|
160
|
+
- spec/spec_helper.rb
|
161
|
+
- structural.gemspec
|
162
|
+
homepage: https://github.com/onthebeach/structural
|
163
|
+
licenses:
|
164
|
+
- MIT
|
165
|
+
metadata: {}
|
166
|
+
post_install_message:
|
167
|
+
rdoc_options: []
|
168
|
+
require_paths:
|
169
|
+
- lib
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - '>='
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - '>='
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
requirements: []
|
181
|
+
rubyforge_project:
|
182
|
+
rubygems_version: 2.2.2
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: Simple models based on hashes
|
186
|
+
test_files:
|
187
|
+
- spec/lib/structural/model/association_spec.rb
|
188
|
+
- spec/lib/structural/model/field_spec.rb
|
189
|
+
- spec/lib/structural/model/type_casts_spec.rb
|
190
|
+
- spec/lib/structural/model_spec.rb
|
191
|
+
- spec/lib/structural/timestamps_spec.rb
|
192
|
+
- spec/spec_helper.rb
|
193
|
+
has_rdoc:
|