memoria 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.
@@ -0,0 +1,119 @@
1
+ require 'memoria/configuration'
2
+ require 'memoria/snapshot'
3
+ require 'memoria/snapshot_saver'
4
+ require 'memoria/version'
5
+
6
+ # Encapsulates and provides access to the public interface of the gem.
7
+ module Memoria
8
+ extend self
9
+
10
+ # The last recorded snapshot.
11
+ #
12
+ # @example
13
+ # Memoria.record('name-of-snapshot')
14
+ # Memoria.current_snapshot # => #<Memoria::Snapshot:0x00007fc5610a17b0 @name="name-of-snapshot">
15
+ #
16
+ # @return [Snapshot] The current snapshot or nil if there is no current snapshot.
17
+ #
18
+ # @api public
19
+ #
20
+ def current_snapshot
21
+ snapshots.last
22
+ end
23
+
24
+ # Configures Memoria.
25
+ #
26
+ # @example
27
+ # Memoria.configure do |config|
28
+ # config.snapshot_extension = 'snappy'
29
+ # config.snapshot_record_mode = :new_episodes
30
+ # config.add_setting :custom_setting_name do
31
+ # Time.now
32
+ # end
33
+ # end
34
+ #
35
+ # @yield The configuration block.
36
+ # @yieldparam config [Configuration] The configuration object.
37
+ # @return [void]
38
+ #
39
+ # @see Memoria::Configuration The configuration object and its available configuration options.
40
+ #
41
+ # @api public
42
+ #
43
+ def configure
44
+ yield configuration
45
+ end
46
+
47
+ # Exposes the gem's configuration.
48
+ #
49
+ # @example
50
+ # Memoria.configuration.snapshot_extension # => 'snap'
51
+ #
52
+ # @return [Configuration] The Memoria configuration.
53
+ #
54
+ # @api public
55
+ #
56
+ def configuration
57
+ @configuration ||= Configuration.new
58
+ end
59
+
60
+ # Returns the list of recorded snapshots.
61
+ #
62
+ # @example
63
+ # Memoria.record('name-of-snapshot-one')
64
+ # Memoria.snapshots # => [#<Memoria::Snapshot:0x00007fc7a3870808 @name="name-of-snapshot-one">]
65
+ #
66
+ # @return [Array<Snapshot>] An array of recorded snapshots.
67
+ #
68
+ # @api public
69
+ #
70
+ def snapshots
71
+ @snapshots ||= []
72
+ end
73
+
74
+ # Returns an entity to persist snapshots (on the file system).
75
+ #
76
+ # @example
77
+ # snapshot_saver = Memoria.snapshot_saver
78
+ # snapshot_saver.save(Snapshot.new('index-page'))
79
+ #
80
+ # @return [SnapshotSaver] Reads and writes snapshots to the disk.
81
+ #
82
+ # @api public
83
+ #
84
+ def snapshot_saver
85
+ @snapshot_saver ||= SnapshotSaver.new(configuration)
86
+ end
87
+
88
+ # Records a new snapshot.
89
+ #
90
+ # @example
91
+ # Memoria.record('name-of-snapshot')
92
+ # Memoria.current_snapshot # => #<Memoria::Snapshot:0x00007fc5610a17b0 @name="name-of-snapshot">
93
+ #
94
+ # @param [String] snapshot_name The name of the snapshot.
95
+ # @return [Snapshot] A new snapshot.
96
+ #
97
+ # @api public
98
+ #
99
+ def record(snapshot_name)
100
+ snapshot = Snapshot.new(snapshot_name)
101
+ snapshots.push(snapshot)
102
+ end
103
+
104
+ # Stops recording snapshots.
105
+ #
106
+ # @example
107
+ # Memoria.record('name-of-snapshot')
108
+ # Memoria.current_snapshot # => #<Memoria::Snapshot:0x00007fc5610a17b0 @name="name-of-snapshot">
109
+ # Memoria.stop_recording
110
+ # Memoria.current_snapshot # => nil
111
+ #
112
+ # @return [Array] An empty array.
113
+ #
114
+ # @api public
115
+ #
116
+ def stop_recording
117
+ snapshots.clear
118
+ end
119
+ end
@@ -0,0 +1,112 @@
1
+ require 'memoria/errors'
2
+
3
+ module Memoria
4
+ # Stores the configuration of the gem.
5
+ class Configuration
6
+ # The only allowed snapshot record modes.
7
+ VALID_SNAPSHOT_RECORD_MODES = %i[all new_episodes none].freeze
8
+
9
+ # @return [String] Directory where the snapshots will be saved
10
+ # @api public
11
+ #
12
+ attr_accessor :snapshot_directory
13
+
14
+ # @return [String] The way the snapshots are recorded.
15
+ # @api public
16
+ #
17
+ attr_reader :snapshot_record_mode
18
+
19
+ # @return [String] File extension of new snapshots.
20
+ # @api public
21
+ #
22
+ attr_reader :snapshot_extension
23
+
24
+ # Creates an instance of the configuration.
25
+ #
26
+ # @example
27
+ # configuration = Memoria::Configuration.new
28
+ #
29
+ # @return [Configuration] An instance of +Configuration+.
30
+ #
31
+ # @api public
32
+ #
33
+ def initialize
34
+ self.snapshot_extension = 'snap'
35
+ self.snapshot_record_mode = :new_episodes
36
+ end
37
+
38
+ # Sets the extension for new snapshots.
39
+ #
40
+ # @param [Symbol] snapshot_extension The new snapshot extension.
41
+ # @raise [Errors::InvalidSnapshotExtension] If the snapshot extension is not valid.
42
+ #
43
+ # @return [String] The given snapshot extension.
44
+ #
45
+ # @api public
46
+ #
47
+ def snapshot_extension=(snapshot_extension)
48
+ validate_snapshot_extension(snapshot_extension)
49
+ @snapshot_extension = snapshot_extension
50
+ end
51
+
52
+ # Sets the snapshot record mode. TODO
53
+ #
54
+ # @param [Symbol] record_mode The new snapshot record mode.
55
+ # @raise [Errors::InvalidRecordMode] If record mode is not one of +VALID_SNAPSHOT_RECORD_MODES+
56
+ #
57
+ # @return [String] The given snapshot extension.
58
+ #
59
+ # @api public
60
+ #
61
+ def snapshot_record_mode=(record_mode)
62
+ validate_record_mode(record_mode)
63
+ @snapshot_record_mode = record_mode
64
+ end
65
+
66
+ # Adds a new setting with the given +name+.
67
+ #
68
+ # @example Configuring a setting directly.
69
+ # configuration = Memoria::Configuration
70
+ # configuration.add_setting(:now) do
71
+ # Time.now
72
+ # end
73
+ #
74
+ # @example Configuring a setting through Memoria's DSL.
75
+ # Memoria.configure do |config|
76
+ # config.add_setting(:now) do
77
+ # Time.now
78
+ # end
79
+ # end
80
+ #
81
+ # @param [Symbol] name The name of the setting.
82
+ # @param [Symbol] block The block to be executed when the setting is called.
83
+ # @raise [Errors::DuplicateSetting] If there is already a setting defined with that name.
84
+ #
85
+ # @return [Symbol] The name of the setting
86
+ #
87
+ # @api public
88
+ #
89
+ def add_setting(name, &block)
90
+ validate_setting_existence(name)
91
+ define_singleton_method(name, block)
92
+ end
93
+
94
+ private
95
+
96
+ # Checks if there is a setting already defined with the given +setting_name.
97
+ def validate_setting_existence(setting_name)
98
+ error_message = "There is already a setting defined with the name #{setting_name}"
99
+ raise Errors::DuplicateSetting, error_message if respond_to?(setting_name)
100
+ end
101
+
102
+ # Checks if the snapshot record mode is one of the modes declared in +VALID_SNAPSHOT_RECORD_MODES+.
103
+ def validate_record_mode(record_mode)
104
+ raise Errors::InvalidRecordMode unless VALID_SNAPSHOT_RECORD_MODES.include?(record_mode.to_sym)
105
+ end
106
+
107
+ # Checks if the snapshot extension is valid.
108
+ def validate_snapshot_extension(snapshot_extension)
109
+ raise Errors::InvalidSnapshotExtension unless snapshot_extension =~ /^[a-zA-Z0-9]+$/
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,4 @@
1
+ require 'memoria/errors/invalid_configuration'
2
+ require 'memoria/errors/invalid_record_mode'
3
+ require 'memoria/errors/invalid_snapshot_extension'
4
+ require 'memoria/errors/duplicate_setting'
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when attempting to add a setting that already exists.
4
+ class DuplicateSetting < InvalidConfiguration
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ "There's already a setting with that name."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when the configuration is invalid. This is a parent class to all the other classes.
4
+ class InvalidConfiguration < RuntimeError
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ "Memoria's configuration is invalid."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when the record mode isn't one of +all+, +none+ or +new_snapshots+.
4
+ class InvalidRecordMode < InvalidConfiguration
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ 'The snapshot record mode is invalid. The only valid modes are :all, :none and :new_snapshots.'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module Errors
3
+ # Raised when the snapshot extension doesn't follow a file naming pattern.
4
+ class InvalidSnapshotExtension < InvalidConfiguration
5
+ # The generic message of the exception.
6
+ #
7
+ # @return [String] Exception's message.
8
+ #
9
+ # @api public
10
+ #
11
+ def message
12
+ 'The snapshot extension is invalid.'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require 'memoria'
2
+ require 'memoria/rspec/configurator'
3
+
4
+ Memoria.configure do |config|
5
+ config.add_setting(:configure_rspec_hooks) do
6
+ Memoria::RSpec::Configurator.configure_rspec_hooks
7
+ end
8
+
9
+ config.add_setting(:include_rspec_matchers) do
10
+ Memoria::RSpec::Configurator.include_rspec_matchers
11
+ end
12
+
13
+ config.snapshot_directory = 'spec/fixtures/snapshots' unless config.snapshot_directory.nil?
14
+ end
@@ -0,0 +1,46 @@
1
+ require 'memoria/rspec/metadata'
2
+ require 'memoria/rspec/metadata_parser'
3
+
4
+ module Memoria
5
+ module RSpec
6
+ # Configures the integration with RSpec.
7
+ module Configurator
8
+ module_function
9
+
10
+ # Configures RSpec's +before+ and +after+ hooks to record snapshots when +match_snapshot+ is called.
11
+ #
12
+ # @return [void]
13
+ #
14
+ # @api private
15
+ #
16
+ def configure_rspec_hooks
17
+ ::RSpec.configure do |config|
18
+ config.before(:each, snapshot: true) do |example|
19
+ current_example = example.respond_to?(:metadata) ? example : example.example
20
+ snapshot_name = Memoria::RSpec::MetadataParser.find_description_for(current_example.metadata)
21
+
22
+ Memoria.record(snapshot_name)
23
+ end
24
+
25
+ config.after(:each, snapshot: true) do
26
+ Memoria.stop_recording
27
+ end
28
+ end
29
+ end
30
+
31
+ # Includes RSpec's matchers such as +match_snapshot+.
32
+ #
33
+ # @return [void]
34
+ #
35
+ # @api private
36
+ #
37
+ def include_rspec_matchers
38
+ require 'memoria/rspec/matcher'
39
+
40
+ ::RSpec.configure do |config|
41
+ config.include Memoria::RSpec::Metadata
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ require 'rspec/expectations'
2
+
3
+ RSpec::Matchers.define :match_snapshot do
4
+ diffable
5
+
6
+ match do |actual|
7
+ snapshot_name = Memoria.current_snapshot.name
8
+
9
+ if snapshot_saver.snapshot_exists?(snapshot_name)
10
+ @expected = snapshot_saver.read(snapshot_name)
11
+ expect(expected).to eq(actual)
12
+ else
13
+ snapshot_saver.write(snapshot_name, actual)
14
+ RSpec.configuration.reporter.message "Generated snapshot: #{snapshot_name}"
15
+ true
16
+ end
17
+ end
18
+
19
+ # :nocov:
20
+ failure_message do
21
+ 'Received value does not match the stored snapshot'
22
+ end
23
+ # :nocov:
24
+
25
+ def snapshot_saver
26
+ Memoria.snapshot_saver
27
+ end
28
+
29
+ attr_reader :expected
30
+ end
@@ -0,0 +1,16 @@
1
+ module Memoria
2
+ module RSpec
3
+ # Provides methods to retrieve RSpec's example metadata
4
+ module Metadata
5
+ # Retrieve the metadata from the current example
6
+ #
7
+ # @api private
8
+ #
9
+ # @return [Hash] RSpec's metadata of the current example.
10
+ #
11
+ def current_example_metadata
12
+ self.class.metadata
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ module Memoria
2
+ module RSpec
3
+ # Extracts information from RSpec's examples metadata.
4
+ module MetadataParser
5
+ extend self
6
+
7
+ # Finds RSpec's description of a given example's metadata.
8
+ #
9
+ # @param [Hash] metadata RSpec's metadata of a given example.
10
+ #
11
+ # @return [String]
12
+ #
13
+ # @api private
14
+ #
15
+ def find_description_for(metadata)
16
+ description = find_description_in(metadata)
17
+ example_group = find_example_group_in(metadata)
18
+
19
+ if example_group
20
+ [find_description_for(example_group), description].join('/')
21
+ else
22
+ description
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # Finds an RSpec description in a given metadata.
29
+ #
30
+ # @param [Hash] metadata RSpec's metadata
31
+ #
32
+ # @return [String] The description of an RSpec example
33
+ #
34
+ def find_description_in(metadata)
35
+ metadata[:description].empty? ? metadata[:scoped_id] : metadata[:description]
36
+ end
37
+
38
+ # Finds an RSpec example group in a given metadata.
39
+ #
40
+ # @param [Hash] metadata RSpec's metadata
41
+ #
42
+ # @return [Hash] The metadata of an RSpec example group
43
+ #
44
+ def find_example_group_in(metadata)
45
+ metadata.key?(:example_group) ? metadata[:example_group] : metadata[:parent_example_group]
46
+ end
47
+ end
48
+ end
49
+ end