Skip to content

Commit

Permalink
Merge pull request #4 from nepalez/release/0.0.6
Browse files Browse the repository at this point in the history
Release/0.0.6
  • Loading branch information
nepalez authored Jun 9, 2019
2 parents 1712a75 + 69e0da4 commit 26e4e4d
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
/tmp/
*.gem
.rspec_status
.idea/
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## WIP

### Added

- Better matching of YAML/JSON files (nepalez)

The loader recognizes complex extensions like `data.yml.erb`
or `data.json.erb`, as well as `data.YAML` in upper register.

- Support for Ruby objects (including Activerecord models) serialization
in the parameters of fixtures (nepalez)

You can send objects, that are stringified in a default Ruby way,
into fixture loaders (seeds, stubs etc.) via ERB bindings.
Those objects will be gracefully inserted into the resulting structure:

```yaml
---
:account: <%= user %>
```
```ruby
let(:user) { FactoryBot.create :user }
subject { load_fixture "#{__dir__}/output.yml", user: user }

# The `user` object has been bound via ERB
it { is_expected.to eq account: user }
```
This feature can be used for adding RSpec [matching arguments](https://relishapp.com/rspec/rspec-mocks/v/3-8/docs/setting-constraints/matching-arguments):
```yaml
---
:foo: <%= foo %>
:bar: 3
```
```ruby
# Use the RSpec `anyting` matcher
subject { { foo: 4, bar: 3 } }

let(:template) { load_fixture "#{__dir__}/template.yml", foo: anyting }

# The `anyting` has been bound via ERB to the template
# Compare `{ foo: 4, bar: 3 }` to the template `{ foo: anything, bar: 3 }`
it { is_expected.to include(template) }
```

**Be careful though**: the trick won't work with objects whose default method `Object#to_s` has been overloaded.

## [0.0.5] - [2018-06-04]

### Added
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ The gem defines 3 helpers (support ERB bindings):
- `seed_fixture(path_to_yaml, **opts)` to prepare database using the [FactoryBot][factory-bot]
- `stub_fixture(path_to_yaml, **opts)` to stub some classes

### Loading

```ruby
# spec/models/user/_spec.rb
RSpec.describe "GraphQL mutation 'deleteProfile'" do
Expand Down Expand Up @@ -71,6 +73,34 @@ RSpec.describe "GraphQL mutation 'deleteProfile'" do
end
```

Notice, that since the `v0.0.6` the gem also supports binding any ruby object, not only strings, booleans and numbers:

```yaml
# ./data.yml
---
account: <%= user %>
```
```ruby
# Bind activerecord model
subject { load_fixture "#{__dir__}/data.yml", user: user }

let(:user) { FactoryBot.create :user }

# The same object will be returned
it { is_expected.to eq account: user }
```
The object must be named in the options you send to the `load_fixture`, `stub_fixture`, or `seed_fixture` helpers.

This feature can also be useful to produce a "partially defined" fixtures with [RSpec argument matchers][rspec-argument-matchers]:

```ruby
subject { load_fixture "#{__dir__}/data.yml", user: kind_of(ActiveRecord::Base) }
```

### Seeding

The seed (`seed_fixture`) file should be a YAML/JSON with opinionated parameters, namely:

- `type` for the name of the [FactoryBot][factory-bot] factory
Expand All @@ -92,6 +122,8 @@ The seed (`seed_fixture`) file should be a YAML/JSON with opinionated parameters
Use the `count: 2` key to create more objects at once.

### Stubbing

Another opinionated format we use for stubs (`stub_fixture`). The gem supports stubbing both message chains and constants.

For message chains:
Expand Down Expand Up @@ -181,3 +213,4 @@ The gem is available as open source under the terms of the [MIT License][license
[factory-bot]: https://github.com/thoughtbot/factory_bot
[rspec]: https://rspec.info/
[dev_to]: https://dev.to/evilmartians/a-fixture-based-approach-to-interface-testing-in-rails-2cd4
[rspec-argument-matchers]: https://relishapp.com/rspec/rspec-mocks/v/3-8/docs/setting-constraints/matching-arguments
8 changes: 2 additions & 6 deletions lib/fixturama.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
module Fixturama
require_relative "fixturama/config"
require_relative "fixturama/utils"
require_relative "fixturama/loader"
require_relative "fixturama/stubs"
require_relative "fixturama/seed"

Expand All @@ -28,12 +29,7 @@ def seed_fixture(path, **opts)
end

def load_fixture(path, **opts)
extname = Pathname.new(path).extname

read_fixture(path, **opts).tap do |content|
return YAML.load(content) if %w[.yaml .yml].include?(extname)
return JSON.parse(content) if extname == ".json"
end
Loader.new(path, opts).call
end

def read_fixture(path, **opts)
Expand Down
80 changes: 80 additions & 0 deletions lib/fixturama/loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#
# Load fixture with some options
#
class Fixturama::Loader
require_relative "loader/value"
require_relative "loader/context"

def call
return load_yaml if yaml?
return load_json if json?

content
end

private

def initialize(path, opts = {})
@path = path
@opts = opts.to_h
end

def basename
@basename ||= Pathname.new(@path).basename.to_s
end

def yaml?
!basename[YAML_EXTENSION].nil?
end

def json?
!basename[JSON_EXTENSION].nil?
end

def context
@context ||= (yaml? || json?) ? Context.new(@opts) : Hashie::Mash.new(@opts)
end

def content
bindings = context.instance_eval { binding }
content = File.read(@path)

ERB.new(content).result(bindings)
end

def load_yaml
finalize YAML.load(content)
end

def load_json
finalize JSON.parse(content)
end

# Takes the nested data loaded from YAML or JSON-formatted fixture,
# and serializes its leafs to the corresponding values from a context
def finalize(data)
case data
when Array
data.map { |val| finalize(val) }
when Hash
data.each_with_object({}) { |(key, val), obj| obj[key] = finalize(val) }
when String
finalize_string(data)
else
data
end
end

# Converts strings of sort `#<Fixturama::Loader::Context[:key]>`
# to the corresponding value by the key
# @param [String] string
# @return [Object]
def finalize_string(string)
key = string.match(Value::MATCHER)&.captures&.first&.to_s
key ? context[key] : string
end

# Matchers for YAML/YML/JSON in file extension like "data.yml.erb" etc.
YAML_EXTENSION = /.+\.ya?ml(\.|\z)/i.freeze
JSON_EXTENSION = /.+\.json(\.|\z)/i.freeze
end
30 changes: 30 additions & 0 deletions lib/fixturama/loader/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class Fixturama::Loader
#
# The context of some fixture
#
class Context
# Get value by key
# @param [#to_s] key
# @return [Object]
def [](key)
@values.send(key).instance_variable_get(:@value)
end

private

def initialize(values)
@values = \
Hash(values).each_with_object(Hashie::Mash.new) do |(key, val), obj|
obj[key] = Value.new(key, val)
end
end

def respond_to_missing?(name, *)
@values.respond_to?(name) || super
end

def method_missing(name, *args)
@values.respond_to?(name) ? @values.send(name, *args) : super
end
end
end
38 changes: 38 additions & 0 deletions lib/fixturama/loader/value.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class Fixturama::Loader
#
# Wraps a value with a reference to its key
# in the [Fixturama::Loader::Context]
#
class Value
# Regex mather to extract value key from the stringified wrapper
MATCHER = /\A\#\<Fixturama::Loader::Context\[([^\]]+)\]\>\z/.freeze

def self.new(key, value)
case value
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass then value
else super
end
end

# The sting representing the value with a reference to it in bindings
def to_s
"\"#<Fixturama::Loader::Context[#{@key}]>\""
end
alias to_str to_s

private

def initialize(key, value)
@key = key
@value = value
end

def method_missing(name, *args, &block)
@value.respond_to?(name) ? @value.send(name, *args, &block) : super
end

def respond_to_missing?(name, *)
@value.respond_to?(name) || super
end
end
end
32 changes: 32 additions & 0 deletions spec/fixturama/load_fixture/_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,41 @@
it { is_expected.to eq expected }
end

context "YAML with ruby object" do
subject { load_fixture("#{__dir__}/data.yaml", id: foobar) }

before { class Test::Foobar; end }

let(:foobar) { Test::Foobar.new }
let(:expected) { { "foo" => { "bar" => foobar } } }

it { is_expected.to eq expected }
end

context "JSON" do
subject { load_fixture("#{__dir__}/data.json", id: 42) }

it { is_expected.to eq expected }
end

context "JSON with ruby object" do
subject { load_fixture("#{__dir__}/data.json", id: foobar) }

before { class Test::Foobar; end }

let(:foobar) { Test::Foobar.new }
let(:expected) { { "foo" => { "bar" => foobar } } }

it { is_expected.to eq expected }
end

context "with RSpec argument matchers" do
subject { load_fixture("#{__dir__}/data.yaml", id: kind_of(Numeric)) }

it "loads the matcher", aggregate_failures: true do
expect("foo" => { "bar" => 42 }).to include subject
expect("foo" => { "bar" => 99 }).to include subject
expect("foo" => { "bar" => :a }).not_to include subject
end
end
end
6 changes: 6 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@
config.expect_with :rspec do |c|
c.syntax = :expect
end

config.around do |example|
module Test; end
example.run
Object.send(:remove_const, :Test)
end
end

0 comments on commit 26e4e4d

Please sign in to comment.