fixtury 0.1.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 73b1b15d6ac9941d265b535f8510cc2896eb4c3fc711af20339b045228e8a888
4
+ data.tar.gz: ca6171cdfc7c721a88dc6e4055db600e10897894295b40022e45e7a55c5c5497
5
+ SHA512:
6
+ metadata.gz: d892685cc0b8a4f54c09c188d1afc0dcc8928008f072c76bf18f0fed7ace0c49d194070d61b65dc4653c528b321f08e4ba3adb59cd2d33a192b4e3f984c09776
7
+ data.tar.gz: cceeab65fe5c416067a7be5ae8fd3c9dc65d73fcf5c812ffc89ba695d60fcae5790414baeea1e43d9c098279e8dddc8a87eede66f275eeb02bfaf11fb1c447f3
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .byebug_history
10
+ *.gem
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.1
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.3.7
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in fixtury.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,64 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fixtury (0.1.0.alpha)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activesupport (6.0.1)
10
+ concurrent-ruby (~> 1.0, >= 1.0.2)
11
+ i18n (>= 0.7, < 2)
12
+ minitest (~> 5.1)
13
+ tzinfo (~> 1.1)
14
+ zeitwerk (~> 2.2)
15
+ ansi (1.5.0)
16
+ autotest (5.0.0)
17
+ minitest-autotest (~> 1.0)
18
+ builder (3.2.3)
19
+ byebug (11.0.1)
20
+ concurrent-ruby (1.1.5)
21
+ globalid (0.4.2)
22
+ activesupport (>= 4.2.0)
23
+ i18n (1.7.0)
24
+ concurrent-ruby (~> 1.0)
25
+ metaclass (0.0.4)
26
+ minitest (5.13.0)
27
+ minitest-autotest (1.1.1)
28
+ minitest-server (~> 1.0)
29
+ path_expander (~> 1.0)
30
+ minitest-reporters (1.4.2)
31
+ ansi
32
+ builder
33
+ minitest (>= 5.0)
34
+ ruby-progressbar
35
+ minitest-server (1.0.5)
36
+ minitest (~> 5.0)
37
+ mocha (1.8.0)
38
+ metaclass (~> 0.0.1)
39
+ path_expander (1.1.0)
40
+ rake (10.5.0)
41
+ ruby-progressbar (1.10.1)
42
+ sqlite (1.0.2)
43
+ thread_safe (0.3.6)
44
+ tzinfo (1.2.5)
45
+ thread_safe (~> 0.1)
46
+ zeitwerk (2.2.2)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ autotest
53
+ bundler (~> 2.0)
54
+ byebug
55
+ fixtury!
56
+ globalid
57
+ minitest (~> 5.0)
58
+ minitest-reporters
59
+ mocha
60
+ rake (~> 10.0)
61
+ sqlite
62
+
63
+ BUNDLED WITH
64
+ 2.0.2
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # Fixtury
2
+
3
+ The goal of this library is to provide an interface for accessing fixture data on-demand rather than having to manage all resources up front. By centralizing and wrapping the definitions of data generation, we can preload and optimize how we load data, yet allow deferred behaviors when desired.
4
+
5
+ For example, if a developer is running a test locally, there's no reason to build all fixtures for your suite.
6
+
7
+ ```
8
+ class MyTest < ::Test
9
+
10
+ fixtury "users.fresh"
11
+ let(:user) { fixtury("users.fresh") }
12
+
13
+ def test_whatever
14
+ assert_eq "Doug", user.first_name
15
+ end
16
+
17
+ end
18
+
19
+ ```
20
+
21
+ Loading this file would ensure `users.fresh` is loaded into the fixture set before the suite is run. In the context of ActiveSupport::TestCase, the Fixtury::Hooks file will ensure the database records are present prior to your suite running. Setting `use_transactional_fixtures` ensures all records are rolled back prior to running another test.
22
+
23
+ In a CI environment, we'd likely want to preload all fixtures. This can be done by requiring all the test files, then telling the fixtury store to load all definitions.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fixtury"
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
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/fixtury.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "fixtury/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "fixtury"
9
+ spec.version = Fixtury::VERSION
10
+ spec.authors = ["Mike Nelson"]
11
+ spec.email = ["mike@guideline.com"]
12
+
13
+ spec.summary = "Treat fixtures like factories and factories like fixtures"
14
+ spec.homepage = "https://github.com/guideline-tech/fixtury"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "autotest"
31
+ spec.add_development_dependency "bundler", "~> 2.0"
32
+ spec.add_development_dependency "byebug"
33
+ spec.add_development_dependency "globalid"
34
+ spec.add_development_dependency "minitest", "~> 5.0"
35
+ spec.add_development_dependency "minitest-reporters"
36
+ spec.add_development_dependency "mocha"
37
+ spec.add_development_dependency "rake", "~> 10.0"
38
+ spec.add_development_dependency "sqlite"
39
+ end
data/lib/fixtury.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/module/attribute_accessors"
5
+ require "active_support/core_ext/module/delegation"
6
+ require "fixtury/version"
7
+ require "fixtury/schema"
8
+ require "fixtury/locator"
9
+ require "fixtury/store"
10
+ require "fixtury/execution_context"
11
+
12
+ module Fixtury
13
+
14
+ def self.define(&block)
15
+ schema.define(&block)
16
+ schema
17
+ end
18
+
19
+ def self.schema
20
+ @top_level_schema ||= ::Fixtury::Schema.new(parent: nil, name: "")
21
+ end
22
+
23
+ end
24
+
25
+ require "fixtury/railtie" if defined?(Rails)
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+ class Definition
5
+
6
+ attr_reader :name
7
+ attr_reader :schema
8
+
9
+ attr_reader :callable
10
+ attr_reader :enhancements
11
+
12
+ def initialize(schema: nil, name:, &block)
13
+ @name = name
14
+ @schema = schema
15
+ @callable = block
16
+ @enhancements = []
17
+ end
18
+
19
+ def enhance(&block)
20
+ @enhancements << block
21
+ end
22
+
23
+ def enhanced?
24
+ @enhancements.any?
25
+ end
26
+
27
+ def call(store: nil, execution_context: nil)
28
+ maybe_set_store_context(store: store) do
29
+ value = run_callable(store: store, callable: callable, execution_context: execution_context, value: nil)
30
+ enhancements.each do |e|
31
+ value = run_callable(store: store, callable: e, execution_context: execution_context, value: value)
32
+ end
33
+ value
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def maybe_set_store_context(store:)
40
+ return yield unless store
41
+
42
+ store.with_relative_schema(schema) do
43
+ yield
44
+ end
45
+ end
46
+
47
+ def run_callable(store:, callable:, execution_context:, value:)
48
+ execution_context ||= self
49
+
50
+ args = []
51
+ args << value unless value.nil?
52
+ if callable.arity > args.length
53
+ raise ArgumentError, "A store store must be provided if the definition expects it." unless store
54
+
55
+ args << store
56
+ end
57
+
58
+ if args.length.positive?
59
+ execution_context.instance_exec(*args, &callable)
60
+ else
61
+ execution_context.instance_eval(&callable)
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+ module Errors
5
+ class AlreadyDefinedError < ::StandardError
6
+
7
+ def initialize(name)
8
+ super("An element identified by `#{name}` already exists.")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+ module Errors
5
+ class CircularDependencyError < ::StandardError
6
+
7
+ def initialize(name)
8
+ super("One of the depdencies of #{name} is dependent on #{name}.")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+ module Errors
5
+ class FixtureNotDefinedError < ::StandardError
6
+
7
+ def initialize(name)
8
+ super("A fixture identified by `#{name}` does not exist.")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Fixtury
2
+ module Errors
3
+ class SchemaFrozenError < ::StandardError
4
+
5
+ def initialize
6
+ super("Schema is frozen. New namespaces, definitions, and enhancements are not allowed.")
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Fixtury
2
+ module Errors
3
+ class UnrecognizableLocatorError < ::StandardError
4
+
5
+ def initialize(action, thing)
6
+ super("Locator did not reognize #{thing} during #{action}")
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # a class made available so helper methods can be provided within the fixture dsl
4
+ module Fixtury
5
+ class ExecutionContext
6
+
7
+ end
8
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fixtury/store"
4
+
5
+ module Fixtury
6
+ module Hooks
7
+
8
+ extend ::ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :fixtury_dependencies
12
+ self.fixtury_dependencies = Set.new
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def fixtury(*names)
18
+ self.fixtury_dependencies += names.flatten.compact.map(&:to_s)
19
+ end
20
+
21
+ end
22
+
23
+ def fixtury(name)
24
+ raise ArgumentError unless self.fixtury_dependencies.include?(name.to_s)
25
+
26
+ ::Fixtury::Store.instance.get(name)
27
+ end
28
+
29
+ def fixtury_loaded?(name)
30
+ ::Fixtury::Store.instance.loaded?(name)
31
+ end
32
+
33
+ def fixtury_database_connections
34
+ ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
35
+ end
36
+
37
+ # piggybacking activerecord fixture setup for now.
38
+ def setup_fixtures(*args)
39
+ if fixtury_dependencies.any?
40
+ setup_fixtury_fixtures
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ # piggybacking activerecord fixture setup for now.
47
+ def teardown_fixtures(*args)
48
+ if fixtury_dependencies.any?
49
+ teardown_fixtury_fixtures
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ def setup_fixtury_fixtures
56
+ return unless use_transactional_fixtures
57
+
58
+ clear_expired_fixtury_fixtures!
59
+ load_all_fixtury_fixtures!
60
+
61
+ fixtury_database_connections.each do |conn|
62
+ conn.begin_transaction joinable: false
63
+ end
64
+ end
65
+
66
+ def teardown_fixtury_fixtures
67
+ return unless use_transactional_fixtures
68
+
69
+ fixtury_database_connections.each(&:rollback_transaction)
70
+ end
71
+
72
+ def clear_expired_fixtury_fixtures!
73
+ ::Fixtury::Store.instance.clear_expired_references!
74
+ end
75
+
76
+ def load_all_fixtury_fixtures!
77
+ fixtury_dependencies.each do |name|
78
+ fixtury(name) unless fixtury_loaded?(name)
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+ class Locator
5
+
6
+ class << self
7
+
8
+ attr_accessor :instance
9
+
10
+ def instance
11
+ @instance ||= begin
12
+ require "fixtury/locator_backend/memory"
13
+ ::Fixtury::Locator.new(
14
+ backend: ::Fixtury::LocatorBackend::Memory.new
15
+ )
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ attr_reader :backend
22
+
23
+ def initialize(backend:)
24
+ @backend = backend
25
+ end
26
+
27
+ def load(ref)
28
+ raise ArgumentError, "Unable to load a nil ref" if ref.nil?
29
+
30
+ backend.load(ref)
31
+ end
32
+
33
+ def dump(value)
34
+ raise ArgumentError, "Unable to dump a nil value" if value.nil?
35
+
36
+ ref = backend.dump(value)
37
+ raise ArgumentError, "The value resulted in a nil ref" if ref.nil?
38
+
39
+ ref
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fixtury/errors/unrecognizable_locator_error"
4
+
5
+ module Fixtury
6
+ module LocatorBackend
7
+ module Common
8
+
9
+ def recognized_reference?(_ref)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def recognized_value?(_value)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def load_recognized_reference(_ref)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def dump_recognized_value(_value)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def load(ref)
26
+ return load_recognized_reference(ref) if recognized_reference?(ref)
27
+
28
+ case ref
29
+ when Array
30
+ ref.map { |subref| self.load(subref) }
31
+ when Hash
32
+ ref.each_with_object({}) do |(k, subref), h|
33
+ h[k] = self.load(subref)
34
+ end
35
+ else
36
+ raise ::Fixtury::Errors::UnrecognizableLocatorError.new(:load, ref)
37
+ end
38
+ end
39
+
40
+ def dump(value)
41
+ return dump_recognized_value(value) if recognized_value?(value)
42
+
43
+ case value
44
+ when Array
45
+ value.map { |subvalue| dump(subvalue) }
46
+ when Hash
47
+ ref.each_with_object({}) do |(k, subvalue), h|
48
+ h[k] = dump(subvalue)
49
+ end
50
+ else
51
+ raise ::Fixtury::Errors::UnrecognizableLocatorError.new(:dump, value)
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./common"
4
+ require "globalid"
5
+
6
+ module Fixtury
7
+ module LocatorBackend
8
+ class GlobalID
9
+
10
+ include ::Fixtury::LocatorBackend::Common
11
+
12
+ MATCHER = %r{^gid://}.freeze
13
+
14
+ def recognized_reference?(ref)
15
+ ref.is_a?(String) && MATCHER.match?(ref)
16
+ end
17
+
18
+ def recognized_value?(val)
19
+ val.respond_to?(:to_global_id)
20
+ end
21
+
22
+ def load_recognized_reference(ref)
23
+ ::GlobalID::Locator.locate ref
24
+ end
25
+
26
+ def dump_recognized_value(value)
27
+ value.to_global_id.to_s
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./common"
4
+
5
+ module Fixtury
6
+ module LocatorBackend
7
+ class Memory
8
+
9
+ include ::Fixtury::LocatorBackend::Common
10
+
11
+ MATCHER = /^fixtury-oid-(?<object_id>[\d]+)$/.freeze
12
+
13
+ def recognized_reference?(ref)
14
+ ref.is_a?(String) && MATCHER.match?(ref)
15
+ end
16
+
17
+ def recognized_value?(_val)
18
+ true
19
+ end
20
+
21
+ def load_recognized_reference(ref)
22
+ match = MATCHER.match(ref)
23
+ return nil unless match
24
+
25
+ ::ObjectSpace._id2ref(match[:object_id].to_i)
26
+ rescue RangeError
27
+ nil
28
+ end
29
+
30
+ def dump_recognized_value(value)
31
+ "fixtury-oid-#{value.object_id}"
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+ class Path
5
+
6
+ def initialize(namespace:, path:)
7
+ @namespace = namespace.to_s
8
+ @path = path.to_s
9
+ @full_path = (
10
+ @path.start_with?("/") ?
11
+ @path :
12
+ File.expand_path(::File.join(@namespace, @path), "/")
13
+ )
14
+ @segments = @full_path.split("/")
15
+ end
16
+
17
+ def top_level_namespace
18
+ return "" if @segments.size == 1
19
+
20
+ @segments.first
21
+ end
22
+
23
+ def relative?
24
+ @path.start_with?(".")
25
+ end
26
+
27
+ def possible_absolute_paths
28
+ @possible_absolute_paths ||= begin
29
+ out = [@full_path]
30
+ out << @path unless relative?
31
+ out
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+ class Railtie < ::Rails::Railtie
5
+
6
+ rake_tasks do
7
+ load "fixtury/tasks.rake"
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module Fixtury
2
+ class Reference
3
+
4
+ attr_reader :name, :value, :created_at
5
+
6
+ def initialize(name, value)
7
+ @name = name
8
+ @value = value
9
+ @created_at = Time.now.to_i
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fixtury/definition"
4
+ require "fixtury/path"
5
+ require "fixtury/errors/already_defined_error"
6
+ require "fixtury/errors/fixture_not_defined_error"
7
+ require "fixtury/errors/schema_frozen_error"
8
+
9
+ module Fixtury
10
+ class Schema
11
+
12
+ attr_reader :definitions, :children, :name, :parent, :relative_name
13
+
14
+ def initialize(parent:, name:)
15
+ @name = name
16
+ @parent = parent
17
+ @relative_name = @name.split("/").last
18
+ @frozen = false
19
+ reset!
20
+ end
21
+
22
+ def reset!
23
+ @children = {}
24
+ @definitions = {}
25
+ end
26
+
27
+ def freeze!
28
+ @frozen = true
29
+ end
30
+
31
+ def frozen?
32
+ !!@frozen
33
+ end
34
+
35
+ def top_level_schema
36
+ top_level_schema? ? self : parent.top_level_schema
37
+ end
38
+
39
+ def top_level_schema?
40
+ parent.nil?
41
+ end
42
+
43
+ def define(&block)
44
+ ensure_not_frozen!
45
+ instance_eval(&block)
46
+ self
47
+ end
48
+
49
+ # helpful for inspection
50
+ def structure(indent = "")
51
+ out = []
52
+ out << "#{indent}ns:#{relative_name}"
53
+ definitions.keys.sort.each do |key|
54
+ out << "#{indent} defn:#{key}"
55
+ end
56
+
57
+ children.keys.sort.each do |key|
58
+ child = children[key]
59
+ out << child.structure("#{indent} ")
60
+ end
61
+
62
+ out.join("\n")
63
+ end
64
+
65
+ def namespace(name, &block)
66
+ ensure_not_frozen!
67
+ ensure_no_conflict!(name: name, definitions: true, namespaces: false)
68
+
69
+ child = find_or_create_child_schema(name: name)
70
+ child.instance_eval(&block)
71
+ child
72
+ end
73
+
74
+ def fixture(name, &block)
75
+ ensure_not_frozen!
76
+ ensure_no_conflict!(name: name, definitions: true, namespaces: true)
77
+ create_child_definition(name: name, &block)
78
+ end
79
+
80
+ def enhance(name, &block)
81
+ ensure_not_frozen!
82
+ definition = get_definition!(name)
83
+ definition.enhance(&block)
84
+ definition
85
+ end
86
+
87
+ def merge(other_ns)
88
+ ensure_not_frozen!
89
+ other_ns.definitions.each_pair do |name, dfn|
90
+ fixture(name, &dfn.callable)
91
+ dfn.enhancements.each do |e|
92
+ enhance(name, &e)
93
+ end
94
+ end
95
+
96
+ other_ns.children.each_pair do |name, other_ns_child|
97
+ namespace(name) do
98
+ merge(other_ns_child)
99
+ end
100
+ end
101
+
102
+ self
103
+ end
104
+
105
+ def get_definition!(name)
106
+ dfn = get_definition(name)
107
+ raise ::Fixtury::Errors::FixtureNotDefinedError, name unless dfn
108
+
109
+ dfn
110
+ end
111
+
112
+ def get_definition(name)
113
+ path = ::Fixtury::Path.new(namespace: self.name, path: name)
114
+ top_level = top_level_schema
115
+
116
+ dfn = nil
117
+ path.possible_absolute_paths.each do |abs_path|
118
+ *namespaces, definition_name = abs_path.split("/")
119
+
120
+ namespaces.shift if namespaces.first == top_level.name
121
+ target = top_level
122
+
123
+ namespaces.each do |ns|
124
+ next if ns.empty?
125
+
126
+ target = target.children[ns]
127
+ break unless target
128
+ end
129
+
130
+ dfn = target.definitions[definition_name] if target
131
+ return dfn if dfn
132
+ end
133
+
134
+ nil
135
+ end
136
+
137
+ def get_namespace(name)
138
+ path = ::Fixtury::Path.new(namespace: self.name, path: name)
139
+ top_level = top_level_schema
140
+
141
+ path.possible_absolute_paths.each do |abs_path|
142
+ *namespaces, _definition_name = abs_path.split("/")
143
+
144
+ namespaces.shift if namespaces.first == top_level.name
145
+ target = top_level
146
+
147
+ namespaces.each do |ns|
148
+ next if ns.empty?
149
+
150
+ target = target.children[ns]
151
+ break unless target
152
+ end
153
+
154
+ return target if target
155
+ end
156
+
157
+ nil
158
+ end
159
+
160
+ protected
161
+
162
+ def find_child_schema(name:)
163
+ children[name.to_s]
164
+ end
165
+
166
+ def find_or_create_child_schema(name:)
167
+ name = name.to_s
168
+ child = find_child_schema(name: name)
169
+ child ||= begin
170
+ children[name] = begin
171
+ child_name = build_child_name(name: name)
172
+ self.class.new(name: child_name, parent: self)
173
+ end
174
+ end
175
+ end
176
+
177
+ def find_child_definition(name:)
178
+ definitions[name.to_s]
179
+ end
180
+
181
+ def create_child_definition(name:, &block)
182
+ child_name = build_child_name(name: name)
183
+ definition = ::Fixtury::Definition.new(name: child_name, schema: self, &block)
184
+ definitions[name.to_s] = definition
185
+ end
186
+
187
+ def build_child_name(name:)
188
+ name = name&.to_s
189
+ raise ArgumentError, "`name` must be provided" if name.nil?
190
+ raise ArgumentError, "#{name} is invalid. `name` must contain only a-z, A-Z, 0-9, and _." unless name.match(/^[a-zA-Z_0-9]+$/)
191
+
192
+ arr = ["", self.name, name]
193
+ arr.join("/").gsub(%r{/{2,}}, "/")
194
+ end
195
+
196
+ def ensure_no_conflict!(name:, namespaces:, definitions:)
197
+ if definitions
198
+ definition = find_child_definition(name: name)
199
+ raise ::Fixtury::Errors::AlreadyDefinedError, definition.name if definition
200
+ end
201
+
202
+ if namespaces
203
+ ns = find_child_schema(name: name)
204
+ raise ::Fixtury::Errors::AlreadyDefinedError, ns.name if ns
205
+ end
206
+ end
207
+
208
+ def ensure_not_frozen!
209
+ return unless frozen?
210
+
211
+ raise ::Fixtury::Errors::SchemaFrozenError
212
+ end
213
+
214
+ end
215
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "yaml"
5
+ require "fixtury/locator"
6
+ require "fixtury/errors/circular_dependency_error"
7
+ require "fixtury/execution_context"
8
+ require "fixtury/reference"
9
+
10
+ module Fixtury
11
+ class Store
12
+
13
+ cattr_accessor :instance
14
+
15
+ HOLDER = "__BUILDING_FIXTURE__"
16
+
17
+ attr_reader :filepath, :references, :ttl, :auto_refresh_expired
18
+ attr_reader :schema, :locator
19
+ attr_reader :verbose
20
+ attr_reader :execution_context
21
+
22
+ def initialize(
23
+ filepath: nil,
24
+ locator: ::Fixtury::Locator.instance,
25
+ verbose: false,
26
+ ttl: nil,
27
+ schema: nil,
28
+ auto_refresh_expired: false
29
+ )
30
+ @schema = schema || ::Fixtury.schema
31
+ @verbose = verbose
32
+ @locator = locator
33
+ @filepath = filepath
34
+ @references = @filepath && ::File.file?(@filepath) ? ::YAML.load_file(@filepath) : {}
35
+ @execution_context = ::Fixtury::ExecutionContext.new
36
+ @ttl = ttl ? ttl.to_i : ttl
37
+ @auto_refresh_expired = !!auto_refresh_expired
38
+ self.class.instance ||= self
39
+ end
40
+
41
+ def dump_to_file
42
+ return unless filepath
43
+
44
+ ::File.open(filepath, "wb") { |io| io.write(references.to_yaml) }
45
+ end
46
+
47
+ def clear_expired_references!
48
+ return unless ttl
49
+
50
+ references.delete_if do |name, ref|
51
+ is_expired = ref_expired?(ref)
52
+ log { "expiring #{name}" } if is_expired
53
+ is_expired
54
+ end
55
+ end
56
+
57
+ def load_all(schema = self.schema)
58
+ schema.definitions.each_pair do |_key, dfn|
59
+ get(dfn.name)
60
+ end
61
+
62
+ schema.schemas.each_pair do |_key, ns|
63
+ load_all(ns)
64
+ end
65
+ end
66
+
67
+ def clear_cache!(pattern: nil)
68
+ pattern ||= "*"
69
+ pattern = "/" + pattern unless pattern.start_with?("/")
70
+ glob = pattern.ends_with?("*")
71
+ pattern = pattern[0...-1] if glob
72
+ references.delete_if do |key, _value|
73
+ hit = glob ? key.start_with?(pattern) : key == pattern
74
+ log(true) { "clearing #{key}" } if hit
75
+ hit
76
+ end
77
+ dump_to_file
78
+ end
79
+
80
+ def with_relative_schema(schema)
81
+ prior = @schema
82
+ @schema = schema
83
+ yield
84
+ ensure
85
+ @schema = prior
86
+ end
87
+
88
+ def loaded?(name)
89
+ dfn = schema.get_definition!(name)
90
+ full_name = dfn.name
91
+ ref = references[full_name]
92
+ result = ref && ref != HOLDER
93
+ log { result ? "hit #{full_name}" : "miss #{full_name}" }
94
+ result
95
+ end
96
+
97
+ def get(name)
98
+ dfn = schema.get_definition!(name)
99
+ full_name = dfn.name
100
+ ref = references[full_name]
101
+
102
+ if ref == HOLDER
103
+ raise ::Fixtury::Errors::CircularDependencyError, full_name
104
+ end
105
+
106
+ if ref && auto_refresh_expired && ref_expired?(ref)
107
+ log { "refreshing #{full_name}" }
108
+ clear_ref(ref)
109
+ ref = nil
110
+ end
111
+
112
+ value = nil
113
+
114
+ if ref
115
+ log { "hit #{full_name}" }
116
+ value = load_ref(ref.value)
117
+ unless value
118
+ clear_ref(full_name)
119
+ log { "missing #{full_name}" }
120
+ end
121
+ end
122
+
123
+ if value.nil?
124
+ # set the references to HOLDER so any recursive behavior ends up hitting a circular dependency error if the same fixture load is attempted
125
+ references[full_name] = HOLDER
126
+
127
+ value = dfn.call(store: self, execution_context: execution_context)
128
+
129
+ log { "store #{full_name}" }
130
+
131
+ ref = dump_ref(full_name, value)
132
+ ref = ::Fixtury::Reference.new(full_name, ref)
133
+ references[full_name] = ref
134
+ end
135
+
136
+ value
137
+ end
138
+ alias [] get
139
+
140
+ def load_ref(ref)
141
+ locator.load(ref)
142
+ end
143
+
144
+ def dump_ref(_name, value)
145
+ locator.dump(value)
146
+ end
147
+
148
+ def clear_ref(name)
149
+ references.delete(name)
150
+ end
151
+
152
+ def ref_expired?(ref)
153
+ return false unless ttl
154
+
155
+ ref.created_at < (Time.now.to_i - ttl)
156
+ end
157
+
158
+ def log(local_verbose = false, &block)
159
+ return unless verbose || local_verbose
160
+
161
+ puts "[fixtury|store] #{block.call}"
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :fixtury do
4
+ task :setup
5
+
6
+ desc "Clear fixtures from your cache. Accepts a pattern or fixture name such as foo/bar or /foo/*. Default pattern is /*"
7
+ task :clear_cache, [:pattern] => :setup do |_t, args|
8
+ ::Fixtury::Store.instance.clear_cache!(pattern: args[:pattern])
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixtury
4
+
5
+ VERSION = "0.1.0.alpha"
6
+
7
+ end
metadata ADDED
@@ -0,0 +1,202 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fixtury
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Mike Nelson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: autotest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: globalid
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest-reporters
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: mocha
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: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '10.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: sqlite
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description:
140
+ email:
141
+ - mike@guideline.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".ruby-version"
148
+ - ".travis.yml"
149
+ - Gemfile
150
+ - Gemfile.lock
151
+ - README.md
152
+ - Rakefile
153
+ - bin/console
154
+ - bin/setup
155
+ - fixtury.gemspec
156
+ - lib/fixtury.rb
157
+ - lib/fixtury/definition.rb
158
+ - lib/fixtury/errors/already_defined_error.rb
159
+ - lib/fixtury/errors/circular_dependency_error.rb
160
+ - lib/fixtury/errors/fixture_not_defined_error.rb
161
+ - lib/fixtury/errors/schema_frozen_error.rb
162
+ - lib/fixtury/errors/unrecognizable_locator_error.rb
163
+ - lib/fixtury/execution_context.rb
164
+ - lib/fixtury/hooks.rb
165
+ - lib/fixtury/locator.rb
166
+ - lib/fixtury/locator_backend/common.rb
167
+ - lib/fixtury/locator_backend/globalid.rb
168
+ - lib/fixtury/locator_backend/memory.rb
169
+ - lib/fixtury/path.rb
170
+ - lib/fixtury/railtie.rb
171
+ - lib/fixtury/reference.rb
172
+ - lib/fixtury/schema.rb
173
+ - lib/fixtury/store.rb
174
+ - lib/fixtury/tasks.rake
175
+ - lib/fixtury/version.rb
176
+ homepage: https://github.com/guideline-tech/fixtury
177
+ licenses: []
178
+ metadata:
179
+ allowed_push_host: https://rubygems.org
180
+ homepage_uri: https://github.com/guideline-tech/fixtury
181
+ source_code_uri: https://github.com/guideline-tech/fixtury
182
+ changelog_uri: https://github.com/guideline-tech/fixtury
183
+ post_install_message:
184
+ rdoc_options: []
185
+ require_paths:
186
+ - lib
187
+ required_ruby_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">"
195
+ - !ruby/object:Gem::Version
196
+ version: 1.3.1
197
+ requirements: []
198
+ rubygems_version: 3.0.6
199
+ signing_key:
200
+ specification_version: 4
201
+ summary: Treat fixtures like factories and factories like fixtures
202
+ test_files: []