declarative-0.0.9/0000755000175000017500000000000013137575440013040 5ustar pravipravideclarative-0.0.9/README.md0000644000175000017500000000364313137575440014325 0ustar pravipravi# Declarative _DSL for nested schemas._ [![Gem Version](https://badge.fury.io/rb/declarative.svg)](http://badge.fury.io/rb/declarative) # Overview Declarative allows _declaring_ nested schemas. ## Installation Add this line to your application's Gemfile: ```ruby gem 'declarative' ``` ## Declarative::Schema Include this into a class or module to allow defining nested schemas using the popular `::property` DSL. Normally, an abstract base class will define essential configuration. ```ruby class Model extend Declarative::Schema def self.default_nested_class Model end end ``` Concrete schema-users simply derive from the base class. ```ruby class Song < Model property :id property :artist do property :id property :name end end ``` This won't do anything but populate the `::definitions` graph. ```ruby Song.definitions #=> ``` The nested schema will be a subclass of `Model`. ```ruby Song.definitions.get(:artist) #=> ``` ## Overriding Nested Building When declaring nested schemas, per default, Declarative will use its own `Schema::NestedBuilder` to create the nested schema composer. Override `::nested_builder` to define your own way of doing that. ```ruby class Model extend Declarative::Schema def self.default_nested_class Model end def self.nested_builder ->(options) do Class.new(Model) do class_eval &options[:_block] # executes `property :name` etc. on nested, fresh class. end end end end ``` ## Features You can automatically include modules into all nested schemas by using `::feature`. ```ruby class Model extend Declarative::Schema feature Bla ``` ## Defaults ```ruby class Model extend Declarative::Schema defaults visible: true ``` ## Copyright * Copyright (c) 2015 Nick Sutterer declarative-0.0.9/declarative.gemspec0000644000175000017500000000161313137575440016671 0ustar pravipravilib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'declarative/version' Gem::Specification.new do |spec| spec.name = "declarative" spec.version = Declarative::VERSION spec.authors = ["Nick Sutterer"] spec.email = ["apotonick@gmail.com"] spec.summary = %q{DSL for nested schemas.} spec.description = %q{DSL for nested generic schemas with inheritance and refining.} spec.homepage = "" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "minitest" spec.add_development_dependency "minitest-line" end declarative-0.0.9/lib/0000755000175000017500000000000013137575440013606 5ustar pravipravideclarative-0.0.9/lib/declarative.rb0000644000175000017500000000032213137575440016413 0ustar pravipravirequire "declarative/version" require "declarative/definitions" require "declarative/heritage" require "declarative/defaults" require "declarative/schema" require "declarative/deep_dup" module Declarative end declarative-0.0.9/lib/declarative/0000755000175000017500000000000013137575440016071 5ustar pravipravideclarative-0.0.9/lib/declarative/deep_dup.rb0000644000175000017500000000043613137575440020206 0ustar pravipravimodule Declarative class DeepDup def self.call(args) return Array[*dup_items(args)] if args.is_a?(Array) return Hash[dup_items(args)] if args.is_a?(Hash) args end private def self.dup_items(arr) arr.to_a.collect { |v| call(v) } end end enddeclarative-0.0.9/lib/declarative/defaults.rb0000644000175000017500000000236613137575440020234 0ustar pravipravimodule Declarative class Defaults def initialize @static_options = {} @dynamic_options = ->(*) { Hash.new } end # Set default values. Usually called in Schema::defaults. # This can be called multiple times and will "deep-merge" arrays, e.g. `_features: []`. def merge!(hash={}, &block) @static_options = Merge.(@static_options, hash) @dynamic_options = block if block_given? self end # Evaluate defaults and merge given_options into them. def call(name, given_options) # TODO: allow to receive rest of options/block in dynamic block. or, rather, test it as it was already implemented. evaluated_options = @dynamic_options.(name, given_options) options = Merge.(@static_options, evaluated_options) options = options.merge(given_options) end # Private! Don't use this anywhere. # Merges two hashes and joins same-named arrays. This is often needed # when dealing with defaults. class Merge def self.call(a, b) a = a.dup b.each do |k, v| a[k] = v and next unless a.has_key?(k) a[k] = v and next unless a[k].is_a?(Array) a[k] = a[k] += v # only for arrays. end a end end end end declarative-0.0.9/lib/declarative/schema.rb0000644000175000017500000000472013137575440017661 0ustar pravipravirequire "declarative/definitions" require "declarative/defaults" require "declarative/heritage" module Declarative # Include this to maintain inheritable, nested schemas with ::defaults and # ::feature the way we have it in Representable, Reform, and Disposable. # # The schema with its defnitions will be kept in ::definitions. # # Requirements to includer: ::default_nested_class, override building with ::nested_builder. module Schema def self.extended(extender) extender.extend DSL # ::property extender.extend Feature # ::feature extender.extend Heritage::DSL # ::heritage extender.extend Heritage::Inherited # ::included end module DSL def property(name, options={}, &block) heritage.record(:property, name, options, &block) build_definition(name, options, &block) end def defaults(options={}, &block) heritage.record(:defaults, options, &block) _defaults.merge!(options, &block) end def definitions @definitions ||= Definitions.new(definition_class) end def definition_class # TODO: test me. Definitions::Definition end private def build_definition(name, options={}, &block) default_options = {} default_options[:_base] = default_nested_class default_options[:_defaults] = _defaults default_options[:_nested_builder] = nested_builder if block definitions.add(name, default_options.merge(options), &block) end def _defaults @defaults ||= Declarative::Defaults.new end def nested_builder NestedBuilder # default implementation. end NestedBuilder = ->(options) do Class.new(options[:_base]) do # base feature(*options[:_features]) class_eval(&options[:_block]) end end end module Feature # features are registered as defaults using _features, which in turn get translated to # Class.new... { feature mod } which makes it recursive in nested schemas. def feature(*mods) mods.each do |mod| include mod register_feature(mod) end end private def register_feature(mod) heritage.record(:register_feature, mod) # this is only for inheritance between decorators and modules!!! ("horizontal and vertical") defaults.merge!(_features: [mod]) end end end end declarative-0.0.9/lib/declarative/heritage.rb0000644000175000017500000000224113137575440020205 0ustar pravipravirequire "declarative/deep_dup" module Declarative class Heritage < Array # Record inheritable assignments for replay in an inheriting class. def record(method, *args, &block) self << {method: method, args: DeepDup.(args), block: block} # DISCUSS: options.dup. end # Replay the recorded assignments on inheritor. # Accepts a block that will allow processing the arguments for every recorded statement. def call(inheritor, &block) each { |cfg| call!(inheritor, cfg, &block) } end private def call!(inheritor, cfg) yield cfg if block_given? # allow messing around with recorded arguments. inheritor.send(cfg[:method], *cfg[:args], &cfg[:block]) end module DSL def heritage @heritage ||= Heritage.new end end # To be extended into classes using Heritage. Inherits the heritage. module Inherited def inherited(subclass) super heritage.(subclass) end end # To be included into modules using Heritage. When included, inherits the heritage. module Included def included(mod) super heritage.(mod) end end end end declarative-0.0.9/lib/declarative/definitions.rb0000644000175000017500000000350213137575440020731 0ustar pravipravimodule Declarative class Definitions < Hash class Definition def initialize(name, options={}, &block) @options = options.dup @options[:name] = name.to_s end def [](name) @options[name] end def merge!(hash) # TODO: this should return a new Definition instance. @options.merge!(hash) self end def merge(hash) # TODO: should be called #copy. DeepDup.(@options).merge(hash) end end def initialize(definition_class) @definition_class = definition_class super() end def each(&block) # TODO : test me! values.each(&block) end # #add is high-level behavior for Definitions#[]=. # reserved options: # :_features # :_defaults # :_base # :_nested_builder def add(name, options={}, &block) options = options[:_defaults].(name, options) if options[:_defaults] # FIXME: pipeline? base = options[:_base] if options.delete(:inherit) and parent_property = get(name) base = parent_property[:nested] options = parent_property.merge(options) # TODO: Definition#merge end if options[:_nested_builder] options[:nested] = build_nested( options.merge( _base: base, _name: name, _block: block, ) ) end # clean up, we don't want that stored in the Definition instance. [:_defaults, :_base, :_nested_builder, :_features].each { |key| options.delete(key) } self[name.to_s] = @definition_class.new(name, options) end def get(name) self[name.to_s] end private # Run builder to create nested schema (or twin, or representer, or whatever). def build_nested(options) options[:_nested_builder].(options) end end enddeclarative-0.0.9/lib/declarative/version.rb0000644000175000017500000000005313137575440020101 0ustar pravipravimodule Declarative VERSION = "0.0.9" end declarative-0.0.9/lib/declarative/testing.rb0000644000175000017500000000150013137575440020067 0ustar pravipravimodule Declarative module Inspect def inspect string = super if is_a?(Proc) elements = string.split("/") string = "#{elements.first}#{elements.last}" end string.gsub(/0x\w+/, "") end module Schema def inspect definitions.extend(Definitions::Inspect) "Schema: #{definitions.inspect}" end end end module Definitions::Inspect def inspect each { |dfn| dfn.extend(Declarative::Inspect) if dfn[:nested] && dfn[:nested].is_a?(Declarative::Schema::DSL) dfn[:nested].extend(Declarative::Inspect::Schema) else dfn[:nested].extend(Declarative::Definitions::Inspect) if dfn[:nested] end } super end def get(*) super.extend(Declarative::Inspect) end end enddeclarative-0.0.9/.travis.yml0000644000175000017500000000014513137575440015151 0ustar pravipravilanguage: ruby rvm: - 2.3.1 - 1.9.3 gemfile: - Gemfile before_install: - gem install bundler declarative-0.0.9/LICENSE.txt0000644000175000017500000000205613137575440014666 0ustar pravipraviCopyright (c) 2015 Nick Sutterer MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. declarative-0.0.9/.gitignore0000644000175000017500000000016613137575440015033 0ustar pravipravi/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log declarative-0.0.9/Rakefile0000644000175000017500000000032113137575440014501 0ustar pravipravirequire "bundler/gem_tasks" require "rake/testtask" task :default => [:test] Rake::TestTask.new(:test) do |test| test.libs << 'test' test.test_files = FileList['test/*_test.rb'] test.verbose = true end declarative-0.0.9/Gemfile0000644000175000017500000000014013137575440014326 0ustar pravipravisource 'https://rubygems.org' # Specify your gem's dependencies in declarative.gemspec gemspec declarative-0.0.9/test/0000755000175000017500000000000013137575440014017 5ustar pravipravideclarative-0.0.9/test/definitions_test.rb0000644000175000017500000000744513137575440017730 0ustar pravipravirequire "test_helper" class DefinitionsTest < Minitest::Spec NestedBuilder = ->(options) { base = options[:_base] || Declarative::Definitions.new(Declarative::Definitions::Definition) base.instance_exec(&options[:_block]) base } let (:schema) { Declarative::Definitions.new(Declarative::Definitions::Definition).extend(Declarative::Definitions::Inspect) } it "what" do # #add works with name schema.add :id # get works with symbol schema.get(:id).inspect.must_equal '#"id"}>' # get works with string schema.get("id").inspect.must_equal '#"id"}>' # #add with name and options schema.add(:id, unique: true) schema.get(:id).inspect.must_equal '#true, :name=>"id"}>' end it "overwrites old when called twice" do schema.add :id schema.add :id, cool: true schema.inspect.must_equal '{"id"=>#true, :name=>"id"}>}' end it "#add with block" do schema.add :artist, _nested_builder: NestedBuilder do add :name add :band, _nested_builder: NestedBuilder do add :location end end schema.inspect.must_equal '{"artist"=>#{"name"=>#"name"}>, "band"=>#{"location"=>#"location"}>}, :name=>"band"}>}, :name=>"artist"}>}' end it "#add with :nested instead of block" do nested_schema = Declarative::Definitions.new(Declarative::Definitions::Definition) nested_schema.extend(Declarative::Definitions::Inspect) nested_schema.add :name schema.add :artist, nested: nested_schema schema.inspect.must_equal '{"artist"=>#{"name"=>#"name"}>}, :name=>"artist"}>}' end it "#add with inherit: true and block" do schema.add :artist, cool: true, _nested_builder: NestedBuilder do add :name add :band, crazy: nil, _nested_builder: NestedBuilder do add :location end end schema.add :id, unique: true, value: 1 schema.add :artist, uncool: false, _nested_builder: NestedBuilder, inherit: true do add :band, normal: false, _nested_builder: NestedBuilder, inherit: true do add :genre end end schema.add :id, unique: false, inherit: true schema.inspect.must_equal '{"artist"=>#true, :nested=>{"name"=>#"name"}>, "band"=>#nil, :nested=>{"location"=>#"location"}>, "genre"=>#"genre"}>}, :name=>"band", :normal=>false}>}, :name=>"artist", :uncool=>false}>, "id"=>#false, :value=>1, :name=>"id"}>}' end it "#add with nested options followed by inherit: true" do schema.add :id, deserializer: options = { render: false } schema.add :id, inherit: true schema.get(:id)[:deserializer][:parse] = true options.must_equal(render: false) end end class DefinitionTest < Minitest::Spec let (:definition) { Declarative::Definitions::Definition.new(:name) } it "#merge does return deep copy" do options = { render: false } merged = definition.merge(options) definition.merge!(render: true) merged.must_equal(:name=>"name", render: false) end enddeclarative-0.0.9/test/defaults_test.rb0000644000175000017500000000707213137575440017220 0ustar pravipravirequire "test_helper" class DefaultsOptionsTest < Minitest::Spec let (:song) { Struct.new(:title, :author_name, :song_volume, :description).new("Revolution", "Some author", 20, nil) } let (:schema) { Declarative::Definitions.new(Declarative::Definitions::Definition).extend Declarative::Definitions::Inspect } let (:defaults) { Declarative::Defaults.new } describe "hash options combined with dynamic options" do it do defaults.merge!(render_nil: true) do |name| { as: name.to_s.upcase } end schema.add :title, _defaults: defaults schema.add :author_name schema.add :description, _defaults: defaults schema.inspect.must_equal '{"title"=>#true, :as=>"TITLE", :name=>"title"}>, "author_name"=>#"author_name"}>, "description"=>#true, :as=>"DESCRIPTION", :name=>"description"}>}' end end describe "with only dynamic property options" do it do defaults.merge!({}) do |name| { as: name.to_s.upcase } end schema.add :title, _defaults: defaults schema.add :author_name schema.add :description, _defaults: defaults schema.inspect.must_equal '{"title"=>#"TITLE", :name=>"title"}>, "author_name"=>#"author_name"}>, "description"=>#"DESCRIPTION", :name=>"description"}>}' end end describe "with only hashes" do it do defaults.merge!(render_nil: true) schema.add :title, _defaults: defaults schema.add :author_name schema.add :description, _defaults: defaults schema.inspect.must_equal '{"title"=>#true, :name=>"title"}>, "author_name"=>#"author_name"}>, "description"=>#true, :name=>"description"}>}' end end describe "#add options win" do it do defaults.merge!(render_nil: true) do |name| { as: name.to_s.upcase } end schema.add :title, as: "Title", _defaults: defaults schema.add :author_name schema.add :description, _defaults: defaults schema.inspect.must_equal '{"title"=>#true, :as=>"Title", :name=>"title"}>, "author_name"=>#"author_name"}>, "description"=>#true, :as=>"DESCRIPTION", :name=>"description"}>}' end end describe "multiple Defaults#merge!" do it "merges arrays automatically" do defaults.merge!(a: 1, b: 2) defaults.merge!( b: 3, _features: ["A"]) defaults.merge!( _features: ["B", "C"]) defaults.(nil, {}).inspect.must_equal "{:a=>1, :b=>3, :_features=>[\"A\", \"B\", \"C\"]}" end it "what" do defaults.merge!(_features: ["A"]) do |name, options| { _features: ["B", "D"] } end defaults.(nil, {}).inspect.must_equal "{:_features=>[\"A\", \"B\", \"D\"]}" end end end class DefaultsMergeTest < Minitest::Spec it do a = { a: "a", features: ["b"] } b = { a: "a", features: ["c", "d"], b: "b" } Declarative::Defaults::Merge.(a, b).must_equal({:a=>"a", :features=>["b", "c", "d"], :b=>"b"}) end end declarative-0.0.9/test/schema_test.rb0000644000175000017500000001021413137575440016641 0ustar pravipravirequire "test_helper" class SchemaTest < Minitest::Spec class Decorator extend Declarative::Schema def self.default_nested_class Decorator end end module AddLinks def self.included(includer) super includer.property(:links) end end class Concrete < Decorator defaults render_nil: true do |name| { as: name.to_s.upcase } end feature AddLinks property :artist, cool: true do property :name property :band, crazy: nil do property :location end end property :id, unique: true, value: 1 end it do Concrete.extend(Declarative::Inspect::Schema) Concrete.inspect Concrete.inspect.gsub(/\s/, "").must_equal 'Schema:{ "links"=>#true,:as=>"LINKS",:name=>"links"}>, "artist"=>#true,:as=>"ARTIST",:cool=>true,:nested=>Schema:{ "links"=>#"links"}>, "name"=>#"name"}>, "band"=>#nil,:nested=>Schema:{ "links"=>#"links"}>, "location"=>#"location"}>},:name=>"band"}>},:name=>"artist"}>, "id"=>#true,:as=>"ID",:unique=>true,:value=>1,:name=>"id"}>}'. gsub("\n", "").gsub(/\s/, "") end class InheritingConcrete < Concrete property :uuid end it do InheritingConcrete.extend(Declarative::Inspect::Schema) InheritingConcrete.inspect InheritingConcrete.inspect.gsub(/\s/, "").must_equal 'Schema:{ "links"=>#true,:as=>"LINKS",:name=>"links"}>, "artist"=>#true,:as=>"ARTIST",:cool=>true,:nested=>Schema:{ "links"=>#"links"}>, "name"=>#"name"}>, "band"=>#nil,:nested=>Schema:{ "links"=>#"links"}>, "location"=>#"location"}>},:name=>"band"}>},:name=>"artist"}>, "id"=>#true,:as=>"ID",:unique=>true,:value=>1,:name=>"id"}>, "uuid"=>#true,:as=>"UUID",:name=>"uuid"}>} '. gsub("\n", "").gsub(/\s/, "") end describe "::property still allows passing internal options" do class ConcreteWithOptions < Decorator defaults cool: true # you can pass your own _nested_builder and it will still receive correct, # defaultized options. property :artist, _nested_builder: ->(options) { OpenStruct.new(cool: options[:cool]) } end it do ConcreteWithOptions.extend(Declarative::Inspect::Schema).inspect.must_equal 'Schema: {"artist"=>#true, :nested=>#, :name=>"artist"}>}' end end describe "multiple ::defaults" do class Twin < Decorator module A; end module B; end module D; end defaults a: "a", _features: [A] do |name| { first: 1, _features: [D] } end # DISCUSS: currently, we only allow one dynamic block. defaults b: "b", _features: [B]# do |name, options| # {} #end property :id do end end it do Twin.extend(Declarative::Inspect::Schema).inspect.must_equal 'Schema: {"id"=>#"a", :b=>"b", :first=>1, :nested=>Schema: {}, :name=>"id"}>}' # :_features get merged. Twin.definitions.get(:id)[:nested].is_a? Twin::A Twin.definitions.get(:id)[:nested].is_a? Twin::B Twin.definitions.get(:id)[:nested].is_a? Twin::D end end end declarative-0.0.9/test/test_helper.rb0000644000175000017500000000014013137575440016655 0ustar pravipravirequire "minitest/autorun" require "declarative" require "declarative/testing" require "ostruct"declarative-0.0.9/test/heritage_test.rb0000644000175000017500000000273713137575440017204 0ustar pravipravirequire "test_helper" class HeritageTest < Minitest::Spec P = Proc.new{}.extend(Declarative::Inspect) # #record module RepresenterA extend Declarative::Heritage::DSL # one arg. heritage.record(:representation_wrap=, true) # 2 args. heritage.record(:property, :name, enable: true) # 3 args. heritage.record(:property, :id, {}, &P) end it { RepresenterA.heritage.inspect.must_equal "[{:method=>:representation_wrap=, :args=>[true], :block=>nil}, {:method=>:property, :args=>[:name, {:enable=>true}], :block=>nil}, {:method=>:property, :args=>[:id, {}], :block=>#}]" } describe "dup of arguments" do module B extend Declarative::Heritage::DSL options = {render: true, nested: {render: false}} heritage.record(:property, :name, options, &P) options[:parse] = true options[:nested][:parse] = false end it { B.heritage.inspect.must_equal "[{:method=>:property, :args=>[:name, {:render=>true, :nested=>{:render=>false}}], :block=>#}]" } end describe "#call with block" do let (:heritage) { Declarative::Heritage.new.record(:property, :id, {}) } class CallWithBlock def self.property(name, options) @args = [name, options] end end it do heritage.(CallWithBlock) { |cfg| cfg[:args].last.merge!(_inherited: true) } CallWithBlock.instance_variable_get(:@args).must_equal [:id, {:_inherited=>true}] end end end declarative-0.0.9/CHANGES.md0000644000175000017500000000170513137575440014435 0ustar pravipravi# 0.0.9 * Removing `uber` dependency. # 0.0.8 * When calling `Schema#defaults` (or `Defaults#merge!`) multiple times, same-named arrays will be joined instead of overridden. This fixes a common problem when merging different default settings. * Remove `Defaults#[]` and `Defaults#[]=`. This now happens via `#merge!`. # 0.0.7 * Simplify `Defaults` and remove a warning in Ruby 2.2.3. # 0.0.6 * `Heritage#call` now accepts a block that allows processing the arguments for every recorded statement before replaying them. This provides a hook to inject or change parameters, e.g. to mark a replay as an inheritance. # 0.0.5 * Introduce `Schema::build_definition` as a central entry point for building `Definition` without any heritage involved. # 0.0.4 * Restructured modules, there's always a public `DSL` module now, etc. # 0.0.3 * Internals, only. # 0.0.2 * First usable version with `Declarative::Schema` and friends. TODO: default_nested_class RM