Coder Social home page Coder Social logo

casecommons / with_model Goto Github PK

View Code? Open in Web Editor NEW
150.0 12.0 18.0 272 KB

Dynamically build an Active Record model (with table) within a test context

Home Page: http://www.casebook.net

License: MIT License

Ruby 100.00%
activerecord testing rspec minitest ruby

with_model's Introduction

Gem Version Build Status API Documentation

with_model dynamically builds an Active Record model (with table) before each test in a group and destroys it afterwards.

Development status

with_model is actively maintained. It is quite stable, so while updates may appear infrequent, it is only because none are needed.

Installation

Install as usual: gem install with_model or add gem 'with_model' to your Gemfile. See .github/workflows/ci.yml for supported (tested) Ruby versions.

RSpec

Extend WithModel into RSpec:

require 'with_model'

RSpec.configure do |config|
  config.extend WithModel
end

minitest/spec

Extend WithModel into minitest/spec and set the test runner explicitly:

require 'with_model'

WithModel.runner = :minitest

class Minitest::Spec
  extend WithModel
end

Usage

After setting up as above, call with_model and inside its block pass it a table block and a model block.

require 'spec_helper'

describe "A blog post" do
  module MyModule; end

  with_model :BlogPost do
    # The table block (and an options hash) is passed to Active Record migration’s `create_table`.
    table do |t|
      t.string :title
      t.timestamps null: false
    end

    # The model block is the Active Record model’s class body.
    model do
      include MyModule
      has_many :comments
      validates_presence_of :title

      def self.some_class_method
        'chunky'
      end

      def some_instance_method
        'bacon'
      end
    end
  end

  # with_model classes can have associations.
  with_model :Comment do
    table do |t|
      t.string :text
      t.belongs_to :blog_post
      t.timestamps null: false
    end

    model do
      belongs_to :blog_post
    end
  end

  it "can be accessed as a constant" do
    expect(BlogPost).to be
  end

  it "has the module" do
    expect(BlogPost.include?(MyModule)).to eq true
  end

  it "has the class method" do
    expect(BlogPost.some_class_method).to eq 'chunky'
  end

  it "has the instance method" do
    expect(BlogPost.new.some_instance_method).to eq 'bacon'
  end

  it "can do all the things a regular model can" do
    record = BlogPost.new
    expect(record).not_to be_valid
    record.title = "foo"
    expect(record).to be_valid
    expect(record.save).to eq true
    expect(record.reload).to eq record
    record.comments.create!(:text => "Lorem ipsum")
    expect(record.comments.count).to eq 1
  end

  # with_model classes can have inheritance.
  class Car < ActiveRecord::Base
    self.abstract_class = true
  end

  with_model :Ford, superclass: Car do
  end

  it "has a specified superclass" do
    expect(Ford < Car).to eq true
  end
end

describe "with_model can be run within RSpec :all hook" do
  with_model :BlogPost, scope: :all do
    table do |t|
      t.string :title
    end
  end

  before :all do
    BlogPost.create # without scope: :all these will fail
  end

  it "has been initialized within before(:all)" do
    expect(BlogPost.count).to eq 1
  end
end

describe "another example group" do
  it "does not have the constant anymore" do
    expect(defined?(BlogPost)).to be_falsy
  end
end

describe "with table options" do
  with_model :WithOptions do
    table :id => false do |t|
      t.string 'foo'
      t.timestamps null: false
    end
  end

  it "respects the additional options" do
    expect(WithOptions.columns.map(&:name)).not_to include("id")
  end
end

Requirements

See the gemspec metadata for dependency requirements. RSpec and minitest are indirect dependencies, and with_model should support any maintained version of both.

Thread-safety

  • A unique table name is used for tables generated via with_model/WithModel::Model.new. This allows with_model (when limited to this API) to run concurrently (in processes or threads) with a single database schema. While there is a possibility of collision, it is very small.
  • A user-supplied table name is used for tables generated via with_table/WithModel::Table.new. This may cause collisions at runtime if tests are run concurrently against a single database schema, unless the caller takes care to ensure the table names passed as arguments are unique across threads/processes.
  • Generated models are created in stubbed constants, which are global; no guarantee is made to the uniqueness of a constant, and this may be unsafe.
  • Generated classes are Active Record subclasses:
    • This library makes no guarantee as to the thread-safety of creating Active Record subclasses concurrently.
    • This library makes no guarantee as to the thread-safety of cleaning up Active Record/Active Support’s internals which are polluted upon class creation.

In general, with_model is not guaranteed to be thread-safe, but is, in certain usages, safe to use concurrently across multiple processes with a single database schema.

Versioning

with_model uses Semantic Versioning 2.0.0.

License

Copyright © 2010–2022 Casebook PBC. Licensed under the MIT license, see LICENSE file.

with_model's People

Contributors

amarshall avatar dependabot-preview[bot] avatar dependabot[bot] avatar markedmondson avatar miks avatar nertzy avatar palkan avatar stevenharman avatar unpaktdev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

with_model's Issues

Generated index names on midsize model names are excessively large under AR5+

We've been migrating a project that uses with_mocks in its specs to Rails 5 from 4.2, and are finding that the autogenerated names for indices are now frequently longer than the character limit for our underlying database (PG9.6). This may not be a bug in with_mocks per se, if it's just a result of changes upstream in ActiveRecord, but documenting this behavior would be helpful for future users.

Manually specifying a shorter index name addresses the issue for now.

Breaking example:

require "rails_helper"

describe "index issue" do
  with_model :MidsizeNameChild do
    table do |t|
      t.belongs_to :midsize_name_parent
    end

    model do
      belongs_to :midsize_name_parent
    end
  end

  with_model :MidsizeNameParent do
    model do
      has_many :midsize_name_children
    end
  end

  let(:instance) { MidsizeNameParent.create }

  describe "try to instantiate" do
    it "is an example" do
      instance
    end
  end
end

Produces: Index name 'index_with_model_midsize_name_children_39923_70337190696860_on_midsize_name_parent_id' on table 'with_model_midsize_name_children_39923_70337190696860' is too long; the limit is 63 characters (though UUID will vary)

Working example:

require "rails_helper"

describe "index issue" do
  with_model :MidsizeNameChild do
    table do |t|
      t.belongs_to :midsize_name_parent,
                   index: { name: "shorter_index_name" }
    end

    model do
      belongs_to :midsize_name_parent
    end
  end

  with_model :MidsizeNameParent do
    model do
      has_many :midsize_name_children
    end
  end

  let(:instance) { MidsizeNameParent.create }

  describe "try to instantiate" do
    it "is an example" do
      instance
    end
  end
end

(passes)

Descendants tracking error when `cache_classes = true`

We're getting an error when we have cache_classes = true on CI with Rails 7.0.1:

 RuntimeError:
              DescendantsTracker.clear was disabled because config.cache_classes = true
            # /home/circleci/app/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.1/lib/active_support/descendants_tracker.rb:119:in `clear'
            # /home/circleci/app/vendor/bundle/ruby/2.7.0/gems/with_model-2.1.6/lib/with_model/model.rb:59:in `cleanup_descendants_tracking'
            # /home/circleci/app/vendor/bundle/ruby/2.7.0/gems/with_model-2.1.6/lib/with_model/model.rb:38:in `destroy'
            # /home/circleci/app/vendor/bundle/ruby/2.7.0/gems/with_model-2.1.6/lib/with_model.rb:53:in `block in setup_object'

Minitest support

There seem to be a couple of sticky points to full minitest support. Namely:

a) The Minitest setup block executes before with_model so the class is not accessible. In Minitest, this block is similar to before(:all) in RSpec so used widely in initializing the test scenario.
b) There's a hard-coded call to before in the with_model block, which isn't supported by Minitest. It's possible to get around it with a extend Minitest::Spec::DSL but that seems a bit unfortunate.

Any directions on how best to resolve? I'm happy to submit a PR, just looking for guidance and perhaps willingness to persevere on a solution.

Call of cleanup_descendants_tracking cleanup too much models

Hello,

Regarding the following part of the code:

ActiveSupport::DescendantsTracker.clear([ActiveRecord::Base])

This call has a side effect on our project: all the models that inherit ActiveRecord::Base are not available in the ActiveRecord::Base.descendants list after the upgrade to Rails 7.

I temporary handle the side effect by overriding the following line:
ActiveSupport::DescendantsTracker.clear([ActiveRecord::Base])
ActiveSupport::DescendantsTracker.clear([@model])

Is there a reason to clear all models related to ActiveRecord::Base and not only the current model?

Regards

Rails 4.2.1 NameError: constant not defined

After upgrading to Rails 4.2.1 we are experiencing the following error:

An error occurred in an after hook
    NameError: constant Object::Presenter not defined
    occurred at /Users/foo/.rvm/gems/ruby-2.1.5@foo-rails4/gems/with_model-1.2.1/lib/with_model/constant_stubber.rb:18:in `remove_const'

I haven't had a chance to look into the root cause of this issue but wanted to bring it to your attention right away.

Rails 6.1 support

Currently, the gem has a dependency on activerecord < 6.1. With the release of rails 6.1.0.rc1, it'd be good to make a new release of with_model that allows activerecord 6.1 :)

Record creation fails when one with_model has a foreign key pointing to another with_model

I made you a lil repro script. The child without foreign key is created just fine, the child with foreign key hits an exception.

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "sqlite3"
  gem "combustion"
  gem "with_model"
  gem "minitest"
end

require "combustion"
require "minitest/autorun"
require "with_model"

# A hack to override some Combustion behavior
module Bundler
  def self.require(*)
    # noop
  end
end
ENV['DATABASE_URL'] = "sqlite3::memory:"
ENV['RAILS_ENV'] = "test"
Combustion.initialize! :active_record, database_reset: false, load_schema: false do
  config.enable_reloading = true
end

WithModel.runner = :minitest

class ForeignKeysSpec < Minitest::Spec
  extend WithModel

  with_model :Parent do
    model do
      has_many :children
      has_many :children_with_foreign_key, class_name: "ChildWithForeignKey"
    end
  end

  with_model :Child do
    table do |t|
      t.references :parent
    end

    model do
      belongs_to :parent
    end
  end

  with_model :ChildWithForeignKey do
    table do |t|
      t.references :parent, foreign_key: true
    end

    model do
      belongs_to :parent
    end
  end

  it "creates a child without a foreign key" do
    parent = Parent.create!
    child = parent.children.create!
    assert_equal parent, child.parent
  end

  it "creates a child with a foreign key" do
    parent = Parent.create!
    assert_raises ActiveRecord::StatementInvalid do
      parent.children_with_foreign_key.create!
    end
  end
end

Compatibility with Active Record 5

Travis CI is reporting some problems when running against the latest Rails master (which will eventually become Active Record 5)

Here are the failures: https://travis-ci.org/Casecommons/with_model/jobs/43477610

I did a git bisect on rails and found that the first commit that causes the problems is rails/rails@a975407

Obviously Rails master is nowhere near stable, so this isn't a pressing issue, but I do want to track my progress at getting to the bottom of this. We may need to change how with_model does some things, or perhaps even report an issue back up to the Rails team.

README needed

Right now we are pointing people to the spec to see example usage. But as the spec gets more and more complicated it no longer emphasizes the common use cases. We should write up a quick README.

Callbacks?

I'm trying to write a dummy model to test that callbacks are used properly, but with_model throws an error:

NoMethodError:
  undefined method `_run_before_save_callbacks' for #<Dummy id: nil, created_at: nil, updated_at: nil>
# /home/fletch/.rvm/gems/ruby-2.1.7/gems/activemodel-4.2.4/lib/active_model/attribute_methods.rb:433:in `method_missing'
# /home/fletch/.rvm/gems/ruby-2.1.7/gems/activesupport-4.2.4/lib/active_support/callbacks.rb:81:in `run_callbacks'

I don't know if it's digging too deep into re-implementing the underlying details of ActiveRecord, but it would be nice to be able to use with_model to test callbacks as well.

ActiveRecord model with has_secure_password causes errors

Great library, very useful! I did however run into one problem.

While trying to test an ActiveRecord model with has_secure_password:
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb

I am getting the following error:

Failures:

  1. ActiveRecord behaviors with has_secure_password should be able to be authenticated
    Failure/Error: user = User.create!(:email => '[email protected]', :password => 'foobar', :password_confirmation => 'foobar')
    ActiveRecord::StatementInvalid:
    NoMethodError: undefined method `error' for nil:NilClass: INSERT INTO "with_model_users_27596" ("email", "password_digest") VALUES (?, ?)

I created a failing spec here:

https://github.com/namick/with_model/commit/861a1a4eef042db411f26a95aea46742d525dc77

I am not sure if it is something I am doing wrong or just an edge case... If you can point me in the right direction as to why this might be happening, im happy to fix it and give you a pull request on it.

Cheers

Specs don't run in Ruby 3.1.0 - NoMethodError: super: no superclass method `descendants' for ActiveRecord::Base:Class

❯ ruby -v
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [arm64-darwin21]
❯ rspec --backtrace

An error occurred while loading ./spec/active_record_behaviors_spec.rb.
Failure/Error: ActiveRecord::Base.establish_connection(adapter: adapter, database: ':memory:')

NoMethodError:
  super: no superclass method `descendants' for ActiveRecord::Base:Class
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activerecord-7.0.0/lib/active_record/dynamic_matchers.rb:22:in `method_missing'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/descendants_tracker.rb:90:in `descendants'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:923:in `block in define_callbacks'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:920:in `each'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:920:in `define_callbacks'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activemodel-7.0.0/lib/active_model/validations.rb:50:in `block in <module:Validations>'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/concern.rb:136:in `class_eval'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/concern.rb:136:in `append_features'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/concern.rb:133:in `include'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/concern.rb:133:in `block in append_features'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/concern.rb:133:in `each'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activesupport-7.0.0/lib/active_support/concern.rb:133:in `append_features'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activerecord-7.0.0/lib/active_record/base.rb:309:in `include'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activerecord-7.0.0/lib/active_record/base.rb:309:in `<class:Base>'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activerecord-7.0.0/lib/active_record/base.rb:282:in `<module:ActiveRecord>'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/activerecord-7.0.0/lib/active_record/base.rb:15:in `<top (required)>'
# ./spec/spec_helper.rb:29:in `require'
# ./spec/spec_helper.rb:29:in `<top (required)>'
# <internal:/Users/grant/.rbenv/versions/3.1.0/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
# <internal:/Users/grant/.rbenv/versions/3.1.0/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
# ./spec/active_record_behaviors_spec.rb:3:in `<top (required)>'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:2112:in `load'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:2112:in `load_file_handling_errors'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:1615:in `block in load_spec_files'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:1613:in `each'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:1613:in `load_spec_files'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:102:in `setup'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:86:in `run'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:71:in `run'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:45:in `invoke'
# /Users/grant/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/exe/rspec:4:in `<top (required)>'
# /Users/grant/.rbenv/versions/3.1.0/bin/rspec:25:in `load'
# /Users/grant/.rbenv/versions/3.1.0/bin/rspec:25:in `<main>'

undefined method `stub_const'

I get the following error while running guard and RSpec 2.12.2

Failure/Error: Unable to find matching line from backtrace
     NoMethodError:
       undefined method `stub_const' for #<RSpec::Core::ExampleGroup::Nested_1:0x007fd11c8f7498>

Any idea?

Maybe, release?

Hey there!

Any plans on releasing a new version? The latest release is not compatible with Rails 6 and master is #28.

Feature request: setup model once per per context, not per example

At the moment, it seems to create table on every example (unless I am missing something).

It would be nice to have an option to do it once per with_table declaration, purely to make it faster.


Otherwise, thank you very much for a great gem. I also quite like how it adds the documentation aspect to the spec. Like here for instance:

describe DomainNameValidator do
  with_model :site do
    table do |t|
      t.string :host_name
    end

    model do
      validates :host_name, domain_name: true
    end
  end
  ...

with_model block shows what the table should have in order to use this custom validation and how to use it. Me like!

Jenkins Issue

Hello,

Using jenkins on our setup we get the following errors (rake spec)

An error occurred in an after hook
  NameError: constant Object::Voucher not defined
  occurred at /var/lib/jenkins/jobs/project/workspace/vendor/bundle/ruby/2.1.0/gems/with_model-1.1.0/lib/with_model/constant_stubber.rb:18:in `remove_const'

F
An error occurred in an after hook
  NameError: constant Object::Voucher not defined
  occurred at /var/lib/jenkins/jobs/project/workspace/vendor/bundle/ruby/2.1.0/gems/with_model-1.1.0/lib/with_model/constant_stubber.rb:18:in `remove_const'

F
An error occurred in an after hook
  NameError: constant Object::Voucher not defined
  occurred at /var/lib/jenkins/jobs/project/workspace/vendor/bundle/ruby/2.1.0/gems/with_model-1.1.0/lib/with_model/constant_stubber.rb:18:in `remove_const'

The Voucher is with_model object:

with_model :voucher do
    table do |t|
      t.string :ticket
    end

    model do
      attr_accessor :validation_hash
      validates :ticket, ticket: true
    end
  end

Any idea/solution? Thanks

duplicate model names in sibling contexts pollute each other when one is referenced before defined

The problem here is that posts is referenced in the second context before it is defined so the Post class that is referenced is the one from the previous context. You can verify this by looking at the object ids.

  context "when the model has a circular nested attribute reference" do
    with_model :blog do
      table {}
      model do
        has_many :posts
        accepts_nested_attributes_for :posts
      end
    end

    with_model :post do
      table do |t|
        t.integer :blog_id
      end

      model do
        puts self.object_id
        belongs_to :blog
        accepts_nested_attributes_for :blog
      end
    end

    subject { Post.to_xsd }

    it "should generate a valid XSD" do
      validate_xsd(subject)
    end

  end

  context "when the model has a nested reference that references another nested reference" do
    with_model :blog do
      table {}
      model do
        has_many :posts
        has_many :readers
        accepts_nested_attributes_for :posts
        accepts_nested_attributes_for :readers
      end
    end

    with_model :post do
      table do |t|
        t.integer :blog_id
      end

      model do
        puts self.object_id
        belongs_to :blog
        has_many :readers
        accepts_nested_attributes_for :blog
        accepts_nested_attributes_for :readers
      end
    end

    with_model :reader do
      table do |t|
        t.integer :blog_id
        t.integer :post_id
      end
    end

    subject { Post.to_xsd }

    it "should generate a valid XSD" do
      validate_xsd(subject)
    end

  end
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.