flatter 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f1e1af6544620264587af098834b94ddf548bac7
4
+ data.tar.gz: c48b5d92d0692cb453c84a84782dcfa77ea4c5f7
5
+ SHA512:
6
+ metadata.gz: 05a93c13278b98f3eeb51f27095575d39a713c13c7933c6863a4f9b0cd800709b7de781afd6e36dc147021bfbd459a0977e194efc739d1a4fbec62e4c92cbce6
7
+ data.tar.gz: f9b66855aab233cd339eb94315abb527a81bd3cf188c8a05b679866ddf82c076264b2e906cc131cd174ea19c2a3223e2a06fcc1001c4f4c9ead9aa230df67dfd
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .ruby-gemset
12
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in flatter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Artem Kuzko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Flatter
2
+
3
+ This gem supersedes [FlatMap](https://github.com/TMXCredit/flat_map) gem. With only it's core concepts in mind
4
+ it has been written from complete scratch to provide more pure, clean, extensible code and reliable functionality.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'flatter'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install flatter
21
+
22
+ ## Usage
23
+
24
+ Coming soon.
25
+
26
+ ## Development
27
+
28
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
29
+
30
+ ## Contributing
31
+
32
+ Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko/flatter.
33
+
34
+
35
+ ## License
36
+
37
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
38
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "flatter"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
11
+
12
+ # require "irb"
13
+ # IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/flatter.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'flatter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "flatter"
8
+ spec.version = Flatter::VERSION
9
+ spec.authors = ["Artem Kuzko"]
10
+ spec.email = ["a.kuzko@gmail.com"]
11
+
12
+ spec.summary = %q{Maps a deeply nested model graph to a single object with a flat set of attributes.}
13
+ spec.description = %q{Flatter transforms a deeply nested graph of ActiveModel-like objects
14
+ to a single mapper object that handles all the nested attributes and has a very flexible behavior
15
+ for handling validation, saving routines with a DRY approach.}
16
+ spec.homepage = "https://github.com/akuzko/flatter"
17
+ spec.license = "MIT"
18
+
19
+ spec.required_ruby_version = '>= 2.0.0'
20
+
21
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ spec.bindir = "bin"
23
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "activesupport", ">= 3.2"
27
+ spec.add_dependency "activemodel", ">= 3.0"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.10"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec"
32
+ spec.add_development_dependency "simplecov", ">= 0.9"
33
+ spec.add_development_dependency "pry"
34
+ spec.add_development_dependency "pry-nav"
35
+ end
data/lib/flatter.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+
4
+ require 'active_model'
5
+
6
+ require 'flatter/version'
7
+ require 'flatter/mapping'
8
+ require 'flatter/mapper'
9
+ require 'flatter/extension'
10
+
11
+ module Flatter
12
+ extend Extension::Registrar
13
+
14
+ use :scribe, require: 'flatter/mapping/scribe'
15
+
16
+ def self.configure
17
+ yield self
18
+ end
19
+ end
@@ -0,0 +1,73 @@
1
+ module Flatter
2
+ module Extension
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Registrar
6
+ autoload :Builder
7
+ autoload :Mapping
8
+ autoload :Mapper
9
+ autoload :Factory
10
+
11
+ def register_as(name)
12
+ ::Flatter.extensions[name] = self
13
+ end
14
+ private :register_as
15
+
16
+ def depends_on(*extensions)
17
+ dependencies.concat extensions
18
+ end
19
+ private :depends_on
20
+
21
+ def dependencies
22
+ @dependencies ||= []
23
+ end
24
+
25
+ def hook!
26
+ return false if hooked?
27
+
28
+ use_dependencies
29
+
30
+ mapping.extend!
31
+ mapper.extend!
32
+ factory.extend!
33
+
34
+ hook_callback!
35
+
36
+ @hooked = true
37
+ end
38
+
39
+ def hooked(&block)
40
+ @hook_callback = block
41
+ end
42
+ private :hooked
43
+
44
+ def hook_callback!
45
+ instance_exec(&@hook_callback) if @hook_callback.present?
46
+ end
47
+ private :hook_callback!
48
+
49
+ def use_dependencies
50
+ dependencies.each{ |extension| ::Flatter.use extension }
51
+ end
52
+ private :use_dependencies
53
+
54
+ def hooked?
55
+ !!@hooked
56
+ end
57
+
58
+ def mapping
59
+ @mapping ||= Mapping.new(self)
60
+ end
61
+ private :mapping
62
+
63
+ def mapper
64
+ @mapper ||= Mapper.new(self)
65
+ end
66
+ private :mapper
67
+
68
+ def factory
69
+ @factory ||= Factory.new(self)
70
+ end
71
+ private :factory
72
+ end
73
+ end
@@ -0,0 +1,70 @@
1
+ module Flatter
2
+ class Extension::Builder
3
+ ExtensionBlockAlreadyDefined = Class.new(RuntimeError)
4
+
5
+ def self.extends(target_name)
6
+ @target_name = target_name
7
+ end
8
+
9
+ def self.target_name
10
+ @target_name
11
+ end
12
+
13
+ def initialize(ext)
14
+ @ext = ext
15
+ @new_options = []
16
+ end
17
+
18
+ def add_option(*options)
19
+ @new_options.concat options
20
+ extend(&Proc.new) if block_given?
21
+ end
22
+ alias_method :add_options, :add_option
23
+
24
+ def extend(&block)
25
+ fail ExtensionBlockAlreadyDefined if @extension_block.present?
26
+ @extension_block = block
27
+ end
28
+
29
+ def extends?
30
+ @new_options.present? || @extension_block.present?
31
+ end
32
+
33
+ def extension
34
+ extension = Module.new
35
+ extension.module_eval(new_option_helpers) if @new_options.present?
36
+ extension.module_eval(&@extension_block) if @extension_block.present?
37
+ @ext.const_set(self.class.target_name, extension)
38
+ extension
39
+ end
40
+ private :extension
41
+
42
+ def fail_if_options_defined!
43
+ options_method = self.class.target_name.underscore + '_options'
44
+ options = ::Flatter::Mapper.public_send(options_method)
45
+ already_defined = options & @new_options
46
+
47
+ if already_defined.present?
48
+ fail RuntimeError, "Cannot extend with #{@ext.name}: options #{already_defined} already defined"
49
+ end
50
+ end
51
+ private :fail_if_options_defined!
52
+
53
+ def new_option_helpers
54
+ code = @new_options.map do |option|
55
+ <<-RUBY
56
+ def #{option}
57
+ options[:#{option}]
58
+ end
59
+
60
+ def #{option}?
61
+ options.key?(:#{option})
62
+ end
63
+ RUBY
64
+ end
65
+
66
+ code.join("\n")
67
+ end
68
+ private :new_option_helpers
69
+ end
70
+ end
@@ -0,0 +1,9 @@
1
+ module Flatter
2
+ class Extension::Factory < Extension::Builder
3
+ extends 'Factory'
4
+
5
+ def extend!
6
+ ::Flatter::Mapper::Factory.send(:prepend, extension) if extends?
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Flatter
2
+ class Extension::Mapper < Extension::Builder
3
+ extends 'Mapper'
4
+
5
+ def extend!
6
+ fail_if_options_defined!
7
+
8
+ ::Flatter::Mapper.mapper_options.concat @new_options
9
+ ::Flatter::Mapper.send(:include, extension) if extends?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Flatter
2
+ class Extension::Mapping < Extension::Builder
3
+ extends 'Mapping'
4
+
5
+ def extend!
6
+ fail_if_options_defined!
7
+
8
+ ::Flatter::Mapper.mapping_options.concat @new_options
9
+ ::Flatter::Mapping.send(:prepend, extension) if extends?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module Flatter
2
+ module Extension::Registrar
3
+ UnknownExtensionError = Class.new(ArgumentError)
4
+
5
+ def extensions
6
+ @extensions ||= {}
7
+ end
8
+
9
+ def use(extension_name, **opts)
10
+ require opts[:require] if opts[:require].present?
11
+
12
+ extension = extensions[extension_name]
13
+
14
+ fail UnknownExtensionError, "Unknown extension #{extension_name}" if extension.nil?
15
+
16
+ extension.hook!
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ module Flatter
2
+ class Mapper
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Factory
6
+ autoload :Options
7
+ autoload :Target
8
+ autoload :Mapping
9
+ autoload :Mounting
10
+ autoload :Traits
11
+ autoload :AttributeMethods
12
+ autoload :Persistence
13
+ autoload :ModelName
14
+
15
+ include Options
16
+ include Target
17
+ include Mapping
18
+ include Mounting
19
+ include Traits
20
+ include AttributeMethods
21
+ include ActiveModel::Validations
22
+ include Persistence
23
+ prepend ModelName
24
+
25
+ def self.inherited(subclass)
26
+ subclass.mappings = mappings.dup
27
+ subclass.mountings = mountings.dup
28
+ end
29
+
30
+ def inspect
31
+ to_s
32
+ end
33
+
34
+ def to_ary
35
+ nil
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ module Flatter
2
+ module Mapper::AttributeMethods
3
+ def respond_to_missing?(name, *)
4
+ mapping_names.map{ |name| [name, :"#{name}="] }.flatten.include?(name) || super
5
+ end
6
+
7
+ def method_missing(name, *args, &block)
8
+ return super if @_attribute_methods_defined
9
+
10
+ extend attribute_methods
11
+ @_attribute_methods_defined = true
12
+
13
+ send(name, *args, &block)
14
+ end
15
+
16
+ def attribute_methods
17
+ names = mapping_names
18
+ Module.new do
19
+ names.each do |name|
20
+ define_method(name){ |*args| mapping(name).read(*args) }
21
+
22
+ define_method(:"#{name}="){ |value| mapping(name).write(value) }
23
+ end
24
+ end
25
+ end
26
+ private :attribute_methods
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Flatter
2
+ class Mapper::Factory
3
+ prepend Flatter::Mapper::Target::FactoryMethods
4
+ prepend Flatter::Mapper::Mounting::FactoryMethods
5
+ prepend Flatter::Mapper::Traits::FactoryMethods
6
+ prepend Flatter::Mapper::Options::FactoryMethods
7
+
8
+ attr_reader :name, :options
9
+
10
+ def initialize(name, **options)
11
+ @name, @options = name.to_s, options
12
+ end
13
+
14
+ def mapper_class
15
+ options[:mapper_class] || mapper_class_name.constantize
16
+ end
17
+
18
+ def mapper_class_name
19
+ options[:mapper_class_name] || "#{name.to_s.camelize}Mapper"
20
+ end
21
+
22
+ def create(mapper)
23
+ mapper_class.new(fetch_target_from(mapper))
24
+ end
25
+
26
+ def fetch_target_from(mapper)
27
+ default_target_from(mapper)
28
+ end
29
+
30
+ def default_target_from(mapper)
31
+ mapper.target.public_send(name) if mapper.target.respond_to?(name)
32
+ end
33
+ private :default_target_from
34
+ end
35
+ end
@@ -0,0 +1,76 @@
1
+ module Flatter
2
+ module Mapper::Mapping
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def map(*args, **opts)
7
+ mappings = opts.slice!(*mapping_options)
8
+ mappings_from_array = Hash[*args.zip(args).flatten]
9
+ mappings.merge!(mappings_from_array)
10
+
11
+ define_mappings(mappings, opts)
12
+ end
13
+
14
+ def define_mappings(mappings, options)
15
+ mappings.each do |name, target_attribute|
16
+ self.mappings[name.to_s] =
17
+ Flatter::Mapping::Factory.new(name, target_attribute, options)
18
+ end
19
+ end
20
+ private :define_mappings
21
+
22
+ def mapping_options
23
+ @@mapping_options ||= []
24
+ end
25
+
26
+ def mappings
27
+ @mappings ||= {}
28
+ end
29
+
30
+ def mappings=(val)
31
+ @mappings = val
32
+ end
33
+ end
34
+
35
+ def read
36
+ local_mappings.map(&:read_as_params).inject({}, :merge)
37
+ end
38
+
39
+ def write(params)
40
+ params = params.with_indifferent_access
41
+ local_mappings.each{ |mapping| mapping.write_from_params(params) }
42
+
43
+ params
44
+ end
45
+
46
+ def local_mappings
47
+ @_local_mappings ||= self.class.mappings.values.map{ |factory| factory.create(self) }
48
+ end
49
+
50
+ def mappings
51
+ local_mappings.each_with_object({}) do |mapping, res|
52
+ res[mapping.name] = mapping
53
+ end
54
+ end
55
+
56
+ def mapping_names
57
+ @_mapping_names ||= mappings.keys
58
+ end
59
+
60
+ def writable_mapping_names
61
+ mappings.select{ |_, v| !v.writer? || v.writer != false }.keys
62
+ end
63
+
64
+ def [](name)
65
+ mappings[name.to_s].try(:read)
66
+ end
67
+
68
+ def []=(name, value)
69
+ mappings[name.to_s].try(:write, value)
70
+ end
71
+
72
+ def mapping(name)
73
+ mappings[name.to_s]
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,8 @@
1
+ module Flatter
2
+ module Mapper::ModelName
3
+ def model_name
4
+ target.class.respond_to?(:model_name) ?
5
+ target.class.model_name : super
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,84 @@
1
+ module Flatter
2
+ module Mapper::Mounting
3
+ extend ActiveSupport::Concern
4
+
5
+ module FactoryMethods
6
+ def create(mapper)
7
+ super.tap do |mounting|
8
+ mounting.mounter = mapper
9
+ mounting.name = name
10
+ end
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def mount(name, *args)
16
+ mountings[name.to_s] = Flatter::Mapper::Factory.new(name, *args)
17
+ end
18
+
19
+ def mountings
20
+ @mountings ||= {}
21
+ end
22
+
23
+ def mountings=(val)
24
+ @mountings = val
25
+ end
26
+ end
27
+
28
+ attr_accessor :mounter, :name
29
+
30
+ def full_name
31
+ [mounter.try(:name), name].compact.join('_')
32
+ end
33
+
34
+ def mappings
35
+ super.tap do |mappings|
36
+ inner_mountings.each do |mounting|
37
+ mounting.local_mappings.each do |mapping|
38
+ mappings[mapping.name] = mapping
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def read
45
+ inner_mountings.map(&:read).inject(super, :merge)
46
+ end
47
+
48
+ def write(params)
49
+ super.tap do
50
+ inner_mountings.each{ |mapper| mapper.write(params) }
51
+ end
52
+ end
53
+
54
+ def local_mountings
55
+ class_mountings_for(self.class)
56
+ end
57
+ private :local_mountings
58
+
59
+ def class_mountings_for(klass)
60
+ class_mountings(klass).map{ |factory| factory.create(self) }
61
+ end
62
+ private :class_mountings_for
63
+
64
+ def class_mountings(klass)
65
+ klass.mountings.values
66
+ end
67
+ private :class_mountings
68
+
69
+ def mountings
70
+ @mountings ||= inner_mountings.each_with_object({}) do |mapper, res|
71
+ res[mapper.full_name] = mapper
72
+ end
73
+ end
74
+
75
+ def mounting(name)
76
+ mountings[name.to_s]
77
+ end
78
+
79
+ def inner_mountings
80
+ @_inner_mountings ||= local_mountings.map{ |mount| [mount, mount.inner_mountings] }.flatten
81
+ end
82
+ protected :inner_mountings
83
+ end
84
+ end
@@ -0,0 +1,25 @@
1
+ module Flatter
2
+ module Mapper::Options
3
+ extend ActiveSupport::Concern
4
+
5
+ module FactoryMethods
6
+ def create(*)
7
+ super.tap do |mapper|
8
+ mapper.options.merge! options.slice(*Mapper.mapper_options)
9
+ end
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def mapper_options
15
+ @@mapper_options ||= []
16
+ end
17
+ end
18
+
19
+ attr_reader :options
20
+
21
+ def initialize(*, **options)
22
+ @options = options
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,74 @@
1
+ module Flatter
2
+ module Mapper::Persistence
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ define_callbacks :save
7
+ end
8
+
9
+ delegate :persisted?, to: :target
10
+
11
+ def apply(params)
12
+ write(params)
13
+ valid? && save
14
+ end
15
+
16
+ def valid?(*)
17
+ mappers_chain(:validate).each(&:run_validations!)
18
+ consolidate_errors!
19
+ errors.empty?
20
+ end
21
+
22
+ def run_validations!
23
+ errors.clear
24
+ with_callbacks(:validate)
25
+ end
26
+
27
+ def save
28
+ results = mappers_chain(:save).map(&:run_save!)
29
+ results.all?
30
+ end
31
+
32
+ def run_save!
33
+ with_callbacks(:save){ save_target }
34
+ end
35
+
36
+ def mappers_chain(context)
37
+ root_mountings.dup.unshift(self)
38
+ end
39
+ private :mappers_chain
40
+
41
+ def save_target
42
+ target.respond_to?(:save) ? target.save : true
43
+ end
44
+ protected :save_target
45
+
46
+ def with_callbacks(type, chain = self_mountings, &block)
47
+ current = chain.shift
48
+ current.run_callbacks(type) do
49
+ chain.present? ? with_callbacks(type, chain, &block) : (yield if block_given?)
50
+ end
51
+ end
52
+
53
+ def root_mountings
54
+ inner_mountings.reject(&:trait?)
55
+ end
56
+ private :root_mountings
57
+
58
+ def self_mountings
59
+ local_mountings.select(&:trait?).unshift(self).reverse
60
+ end
61
+ private :self_mountings
62
+
63
+ def consolidate_errors!
64
+ root_mountings.map(&:errors).each do |errs|
65
+ errors.messages.merge!(errs.to_hash){ |key, old, new| old + new }
66
+ end
67
+ end
68
+ private :consolidate_errors!
69
+
70
+ def errors
71
+ trait? ? mounter.errors : super
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,38 @@
1
+ module Flatter
2
+ module Mapper::Target
3
+ NoTargetError = Class.new(ArgumentError)
4
+
5
+ module FactoryMethods
6
+ def fetch_target_from(mapper)
7
+ return super unless options.key?(:target)
8
+
9
+ target = options[:target]
10
+
11
+ case target
12
+ when Proc then target.(mapper.target)
13
+ when String, Symbol
14
+ (mapper.private_methods + mapper.protected_methods + mapper.public_methods).include?(target.to_sym) ?
15
+ mapper.send(target) :
16
+ fail(ArgumentError, "Cannot use target #{target.inspect} with `#{mapper.name}`. Make sure #{target.inspect} is defined for #{mapper}")
17
+ else target
18
+ end
19
+ end
20
+ end
21
+
22
+ attr_reader :target
23
+
24
+ def initialize(target, *)
25
+ unless target.present?
26
+ fail NoTargetError, "Target object is required to initialize #{self.class.name}"
27
+ end
28
+
29
+ super
30
+
31
+ @target = target
32
+ end
33
+
34
+ def set_target(target)
35
+ @target = target
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,155 @@
1
+ module Flatter
2
+ module Mapper::Traits
3
+ extend ActiveSupport::Concern
4
+
5
+ module FactoryMethods
6
+ attr_accessor :extension
7
+
8
+ def traits
9
+ Array(options[:traits])
10
+ end
11
+
12
+ def trait?
13
+ !!options[:trait]
14
+ end
15
+
16
+ def create(*)
17
+ super.tap do |mounting|
18
+ mounting.set_traits(traits)
19
+ mounting.trait! if trait?
20
+ mounting.extend_with(extension) if extension.present?
21
+ end
22
+ end
23
+
24
+ def fetch_target_from(mapper)
25
+ trait? ? mapper.target : super
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ def mount(*, &block)
31
+ super.tap{ |f| f.extension = block }
32
+ end
33
+
34
+ def trait(name, &block)
35
+ trait_name = "#{name}_trait"
36
+ mapper_class = Class.new(Flatter::Mapper, &block)
37
+
38
+ if self.name.present?
39
+ mapper_class_name = trait_name.camelize
40
+ const_set(mapper_class_name, mapper_class)
41
+
42
+ mount trait_name, mapper_class_name: "#{self.name}::#{mapper_class_name}", trait: true
43
+ else
44
+ mount trait_name, mapper_class: mapper_class, trait: true
45
+ end
46
+ end
47
+ end
48
+
49
+ def initialize(target, *traits, **, &block)
50
+ super
51
+
52
+ set_traits(traits)
53
+ extend_with(block) if block.present?
54
+ end
55
+
56
+ def extend_with(extension)
57
+ singleton_class.trait :extension, &extension
58
+ end
59
+
60
+ def set_target(target)
61
+ if trait?
62
+ mounter.set_target(target)
63
+ else
64
+ super
65
+ trait_mountings.each{ |trait| trait.set_target!(target) }
66
+ end
67
+ end
68
+
69
+ def set_target!(target)
70
+ @target = target
71
+ end
72
+ protected :set_target!
73
+
74
+ def full_name
75
+ if name == 'extension_trait'
76
+ super
77
+ else
78
+ name
79
+ end
80
+ end
81
+
82
+ def local_mountings
83
+ @local_mountings ||= class_mountings_for(singleton_class) + super
84
+ end
85
+ private :local_mountings
86
+
87
+ def class_mountings(klass)
88
+ mountings = super.reject do |factory|
89
+ factory.trait? &&
90
+ !(factory.name == 'extension_trait' || trait_names.include?(factory.name))
91
+ end
92
+
93
+ # For a given mountings list, it's trait factories are reordered according to
94
+ # order of the trait names specified for a given object. for example, list
95
+ # [m1, t1, m2, m3, t2, t3, m4] for traits list of [t2, t3, t1] will be
96
+ # transformed to [m1, t2, m2, m3, t3, t1, m4]
97
+ traits = trait_names.map{ |name| mountings.find{ |f| f.name == name } }.compact
98
+
99
+ traits.
100
+ map{ |t| mountings.index(t) }.
101
+ sort.
102
+ reverse.
103
+ each_with_index{ |index, i| mountings[index] = traits[i] }
104
+
105
+ mountings
106
+ end
107
+ private :class_mountings
108
+
109
+ def traits
110
+ @traits ||= []
111
+ end
112
+
113
+ def trait_names
114
+ traits.map{ |trait| "#{trait.to_s}_trait" }
115
+ end
116
+
117
+ def set_traits(traits)
118
+ @traits = traits
119
+ end
120
+
121
+ def trait?
122
+ !!@trait
123
+ end
124
+
125
+ def trait!
126
+ @trait = true
127
+ end
128
+
129
+ def trait_mountings
130
+ @_trait_mountings ||= local_mountings.select(&:trait?)
131
+ end
132
+ private :trait_mountings
133
+
134
+ def shared_methods
135
+ self.class.public_instance_methods(false)
136
+ end
137
+
138
+ def respond_to_missing?(name, *)
139
+ return false if trait?
140
+
141
+ trait_mountings.any? do |trait|
142
+ trait.shared_methods.include?(name)
143
+ end
144
+ end
145
+
146
+ def method_missing(name, *args, &block)
147
+ if trait?
148
+ mounter.send(name, *args, &block)
149
+ else
150
+ trait = trait_mountings.detect{ |trait| trait.shared_methods.include?(name) }
151
+ trait ? trait.send(name, *args, &block) : super
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,43 @@
1
+ module Flatter
2
+ class Mapping
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Factory
6
+ autoload :Scribe
7
+
8
+ attr_reader :mapper, :name, :target_attribute, :options
9
+
10
+ delegate :target, to: :mapper
11
+
12
+ def initialize(mapper, name, target_attribute, **options)
13
+ @mapper = mapper
14
+ @name = name.to_s
15
+ @target_attribute = target_attribute
16
+ @options = options
17
+ end
18
+
19
+ def read
20
+ read!
21
+ end
22
+
23
+ def read!
24
+ target.public_send(target_attribute)
25
+ end
26
+
27
+ def write(value)
28
+ write!(value)
29
+ end
30
+
31
+ def write!(value)
32
+ target.public_send("#{target_attribute}=", value)
33
+ end
34
+
35
+ def read_as_params
36
+ {name => read}
37
+ end
38
+
39
+ def write_from_params(params)
40
+ write(params[name]) if params.key?(name)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ module Flatter
2
+ class Mapping::Factory
3
+ def initialize(*args)
4
+ @args = args
5
+ end
6
+
7
+ def create(mapper)
8
+ Mapping.new(mapper, *@args)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,53 @@
1
+ module Flatter
2
+ module Mapping::Scribe
3
+ extend Flatter::Extension
4
+
5
+ register_as :scribe
6
+
7
+ BadWriterError = Class.new(ArgumentError)
8
+
9
+ mapping.add_options :reader, :writer do
10
+ def read
11
+ return super unless reader?
12
+
13
+ case reader
14
+ when Proc, String, Symbol
15
+ args = Array((name if arity_of(reader) == 1))
16
+ exec_or_send(reader, args)
17
+ when false then nil
18
+ else reader
19
+ end
20
+ end
21
+
22
+ def write(value)
23
+ return super unless writer?
24
+
25
+ case writer
26
+ when Proc, String, Symbol
27
+ args = [value].tap{ |a| a << name if arity_of(writer) == 2 }
28
+ exec_or_send(writer, args)
29
+ when false then nil
30
+ else fail BadWriterError, "cannot use #{writer} for assigning values"
31
+ end
32
+ end
33
+
34
+ def read_as_params
35
+ reader == false ? {} : super
36
+ end
37
+
38
+ def arity_of(obj)
39
+ (obj.is_a?(Proc) ? obj : mapper.method(obj)).arity
40
+ end
41
+ private :arity_of
42
+
43
+ def exec_or_send(obj, args)
44
+ if obj.is_a?(Proc)
45
+ mapper.instance_exec(*args, &obj)
46
+ else
47
+ mapper.send(obj, *args)
48
+ end
49
+ end
50
+ private :exec_or_send
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ module Flatter
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Artem Kuzko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-nav
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: |-
126
+ Flatter transforms a deeply nested graph of ActiveModel-like objects
127
+ to a single mapper object that handles all the nested attributes and has a very flexible behavior
128
+ for handling validation, saving routines with a DRY approach.
129
+ email:
130
+ - a.kuzko@gmail.com
131
+ executables:
132
+ - console
133
+ - setup
134
+ extensions: []
135
+ extra_rdoc_files: []
136
+ files:
137
+ - .gitignore
138
+ - .rspec
139
+ - .travis.yml
140
+ - Gemfile
141
+ - LICENSE.txt
142
+ - README.md
143
+ - Rakefile
144
+ - bin/console
145
+ - bin/setup
146
+ - flatter.gemspec
147
+ - lib/flatter.rb
148
+ - lib/flatter/extension.rb
149
+ - lib/flatter/extension/builder.rb
150
+ - lib/flatter/extension/factory.rb
151
+ - lib/flatter/extension/mapper.rb
152
+ - lib/flatter/extension/mapping.rb
153
+ - lib/flatter/extension/registrar.rb
154
+ - lib/flatter/mapper.rb
155
+ - lib/flatter/mapper/attribute_methods.rb
156
+ - lib/flatter/mapper/factory.rb
157
+ - lib/flatter/mapper/mapping.rb
158
+ - lib/flatter/mapper/model_name.rb
159
+ - lib/flatter/mapper/mounting.rb
160
+ - lib/flatter/mapper/options.rb
161
+ - lib/flatter/mapper/persistence.rb
162
+ - lib/flatter/mapper/target.rb
163
+ - lib/flatter/mapper/traits.rb
164
+ - lib/flatter/mapping.rb
165
+ - lib/flatter/mapping/factory.rb
166
+ - lib/flatter/mapping/scribe.rb
167
+ - lib/flatter/version.rb
168
+ homepage: https://github.com/akuzko/flatter
169
+ licenses:
170
+ - MIT
171
+ metadata: {}
172
+ post_install_message:
173
+ rdoc_options: []
174
+ require_paths:
175
+ - lib
176
+ required_ruby_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
179
+ - !ruby/object:Gem::Version
180
+ version: 2.0.0
181
+ required_rubygems_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - '>='
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ requirements: []
187
+ rubyforge_project:
188
+ rubygems_version: 2.1.11
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: Maps a deeply nested model graph to a single object with a flat set of attributes.
192
+ test_files: []