spec_forge 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/.standard.yml +3 -3
- data/CHANGELOG.md +106 -1
- data/README.md +34 -22
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +91 -14
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -22
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +21 -8
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +27 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +99 -0
- data/lib/spec_forge/runner.rb +132 -123
- data/lib/spec_forge/spec/expectation/constraint.rb +91 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b2d5790d4797a63b8fbad4dd296b9c9beb657b30a98bda5fab8ca7c635abd5a
|
4
|
+
data.tar.gz: b22a8f664dd676846e703f5312383c9c9b1b9cabab52b2cc3c8a8d28830490c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 858e7dfd4546bc34e7dd8f899d99b5cf03d26cfbe9042f238de44566e2981a82bcd06803a0319f6d937d7c509dc3c02e8a962415c8e6fbb8e3c515c693c5e176
|
7
|
+
data.tar.gz: 3b246fda2d4bbbc5ce12fa0f683e27e1cb0e2950fe3b7c03b978f6d88662d1b65b0037b00931fe598415ee3d3c8477f7f76fd3f523547e061f86ac000e617d52
|
data/.standard.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -19,9 +19,114 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
19
19
|
|
20
20
|
### Added
|
21
21
|
|
22
|
+
- Added new context system for managing shared state between tests
|
23
|
+
- Introduced `SpecForge.context` global accessor for accessing test context
|
24
|
+
- Created `Context` class with modular components:
|
25
|
+
- `Context::Global` for file-level shared variables
|
26
|
+
- `Context::Variables` for managing variables with overlay support
|
27
|
+
- `Context::Store` for storing the results of the tests
|
28
|
+
- Added support for defining and referencing global variables
|
29
|
+
```yaml
|
30
|
+
global:
|
31
|
+
variables:
|
32
|
+
api_version: "v2"
|
33
|
+
environment: "test"
|
34
|
+
|
35
|
+
index_user:
|
36
|
+
path: "/{api_version}/users"
|
37
|
+
query:
|
38
|
+
api_version: "global.variables.api_version"
|
39
|
+
```
|
40
|
+
- Added compound matcher support via `matcher.and` for combining multiple matchers
|
41
|
+
```yaml
|
42
|
+
email:
|
43
|
+
matcher.and:
|
44
|
+
- kind_of.string
|
45
|
+
- /@/
|
46
|
+
- matcher.end_with: ".com"
|
47
|
+
```
|
48
|
+
- Added custom RSpec matcher `have_size` for checking an object's size via `matcher.have_size`
|
49
|
+
- Added new `Loader` class for improved spec file processing
|
50
|
+
- Added new `Filter` class for more flexible test filtering
|
51
|
+
- Added normalizer for global context validation
|
52
|
+
- Added line number tracking for specs and expectations
|
53
|
+
- Added support for defining and referencing callbacks
|
54
|
+
```ruby
|
55
|
+
# Configuration level
|
56
|
+
SpecForge.configure do |config|
|
57
|
+
config.register_callback("callback_name") { |context| }
|
58
|
+
# These are aliases
|
59
|
+
# config.define_callback("callback_name") { |context| }
|
60
|
+
# config.callback("callback_name") { |context| }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Module level (no aliases)
|
64
|
+
SpecForge.register_callback("callback_name") { |context| }
|
65
|
+
```
|
66
|
+
Once defined, callbacks can be referenced in spec files via the global context
|
67
|
+
```yaml
|
68
|
+
global:
|
69
|
+
callbacks:
|
70
|
+
- before: callback_name
|
71
|
+
after: cleanup_database_state
|
72
|
+
```
|
73
|
+
- Added support for storing and retrieving test data via the `store_as` directive and `store` attribute
|
74
|
+
```yaml
|
75
|
+
create_user:
|
76
|
+
path: "/users"
|
77
|
+
method: "post"
|
78
|
+
expectations:
|
79
|
+
- variables:
|
80
|
+
name: "John"
|
81
|
+
email: "john@example.com"
|
82
|
+
store_as: "created_user"
|
83
|
+
expect:
|
84
|
+
status: 200
|
85
|
+
|
86
|
+
show_user:
|
87
|
+
path: "/users/:id"
|
88
|
+
query:
|
89
|
+
id: store.created_user.response.id
|
90
|
+
- expect:
|
91
|
+
status: 200
|
92
|
+
```
|
93
|
+
- Added `UndefinedMatcherError` for clearer error messaging when invalid matchers are used
|
94
|
+
- Enhanced debugging capabilities with improved DebugProxy methods and store access
|
95
|
+
- Added HTTP status descriptions for better error messages
|
96
|
+
- Added support for string values in `query`, `body`, and `variables` attributes
|
97
|
+
- Added print statement when filtering tests for better visibility
|
98
|
+
|
22
99
|
### Changed
|
23
100
|
|
24
|
-
|
101
|
+
- Renamed `SpecForge.forge` to `SpecForge.forge_path`
|
102
|
+
- Renamed attribute `http_method` to `http_verb` (`http_method` is now an alias)
|
103
|
+
- Refactored attribute resolution methods:
|
104
|
+
- Renamed `Attribute#resolve` to `#resolved` (memoized version)
|
105
|
+
- Renamed `Attribute#resolve_value` to `#resolve` (immediate resolution)
|
106
|
+
- Added `Attribute#resolve_as_matcher` for resolving attributes into RSpec matchers
|
107
|
+
- Refactored variable resolution to use the new context system
|
108
|
+
- Updated `Runner` to properly initialize and manage context between tests
|
109
|
+
- Improved error messages with more contextual information about the execution environment
|
110
|
+
- Updated YARD comments with better API descriptions and examples
|
111
|
+
- Restructured internal architecture for better separation of concerns
|
112
|
+
- Moved all error classes under `SpecForge::Error`
|
113
|
+
- Fixed issue where nesting expanded matchers (such as `matcher.include`) would cause an error
|
114
|
+
- Improved response body validation for hash expectations:
|
115
|
+
- Each root-level key is now checked individually for more precise error messages
|
116
|
+
- Nested hashes still use the `include` matcher for flexibility
|
117
|
+
- Adjusted `Attribute::Matcher` to accept either `matcher` or `matchers` namespace
|
118
|
+
- Changed empty array matcher from using `contain_exactly` to `eq([])`
|
119
|
+
- Changed empty hash matcher from using `include` to `eq({})`
|
120
|
+
- Changed `forge_and` description from "matches all of:" to "match all:"
|
121
|
+
- Improved error handling for chainable attributes with better descriptions for various object types
|
122
|
+
- Limited error backtrace to 50 lines for cleaner output
|
123
|
+
- Enhanced spec loading error messages with more detailed information
|
124
|
+
- Improved RSpec example descriptions for better test output
|
125
|
+
- Added support for overwriting headers at the request level
|
126
|
+
|
127
|
+
## Removed
|
128
|
+
|
129
|
+
- Removed `Configuration.overlay_options`
|
25
130
|
|
26
131
|
## [0.5.0] - 12025-02-28
|
27
132
|
|
data/README.md
CHANGED
@@ -4,30 +4,50 @@
|
|
4
4
|

|
5
5
|
[](https://github.com/itsthedevman/spec_forge/actions/workflows/main.yml)
|
6
6
|
|
7
|
+
> Note: The code in this repository represents the latest development version with new features and improvements that are being prepared for future releases. For the current stable version, check out [v0.6.0](https://github.com/itsthedevman/spec_forge/releases/tag/v0.6.0) on GitHub releases.
|
8
|
+
|
7
9
|
Write API tests in YAML that read like documentation:
|
8
10
|
|
9
11
|
```yaml
|
10
|
-
|
11
|
-
path: /users/
|
12
|
+
show_user:
|
13
|
+
path: /users/{id}
|
14
|
+
variables:
|
15
|
+
expected_status: 200
|
16
|
+
user_id: 1
|
17
|
+
query:
|
18
|
+
id: variables.user_id
|
12
19
|
expectations:
|
13
20
|
- expect:
|
14
|
-
status:
|
21
|
+
status: variables.expected_status
|
15
22
|
json:
|
16
23
|
name: kind_of.string
|
17
|
-
email:
|
24
|
+
email:
|
25
|
+
matcher.and:
|
26
|
+
- kind_of.string
|
27
|
+
- /@/
|
18
28
|
```
|
19
29
|
|
20
30
|
That's a complete test. No Ruby code, no configuration files, no HTTP client setup - just a clear description of what you're testing. Under the hood, you get all the power of RSpec's matchers, Faker's data generation, and FactoryBot's test objects.
|
21
31
|
|
22
32
|
## Why SpecForge?
|
23
33
|
|
24
|
-
|
34
|
+
1. **Living Documentation**: Your tests should serve as clear, readable documentation of your API's behavior.
|
35
|
+
2. **Reduce Boilerplate**: Write tests without repetitive setup code and HTTP configuration.
|
36
|
+
3. **Quick Setup**: Start testing APIs in minutes instead of spending hours on test infrastructure.
|
37
|
+
4. **Gradual Adoption**: Use alongside your existing test suite, introducing it incrementally where it makes sense.
|
38
|
+
5. **Developer & QA Collaboration**: Create a testing format that everyone can understand and maintain, regardless of Ruby expertise.
|
39
|
+
|
40
|
+
## Key Features
|
25
41
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
42
|
+
- **YAML-Based Tests**: Write clear, declarative tests that read like documentation
|
43
|
+
- **RSpec Integration**: Leverage all the power of RSpec matchers and expectations
|
44
|
+
- **FactoryBot Integration**: Generate test data with FactoryBot integration
|
45
|
+
- **Faker Integration**: Create realistic test data with Faker
|
46
|
+
- **Variable System**: Define and reference variables for dynamic test data
|
47
|
+
- **Context Storage**: Store API responses and reference them in subsequent tests
|
48
|
+
- **Compound Matchers**: Combine multiple validations with `matcher.and` for precise expectations
|
49
|
+
- **Global Variables**: Define shared configuration at the file level
|
50
|
+
- **Callback System**: Hook into the test lifecycle using Ruby for setup, teardown, and much more!
|
31
51
|
|
32
52
|
## When Not to Use SpecForge
|
33
53
|
|
@@ -85,25 +105,17 @@ For comprehensive documentation, visit the [SpecForge Wiki](https://github.com/i
|
|
85
105
|
- [Getting Started Guide](https://github.com/itsthedevman/spec_forge/wiki/Getting-Started)
|
86
106
|
- [Configuration Options](https://github.com/itsthedevman/spec_forge/wiki/Configuration)
|
87
107
|
- [Writing Tests](https://github.com/itsthedevman/spec_forge/wiki/Writing-Tests)
|
108
|
+
- [Running Tests](https://github.com/itsthedevman/spec_forge/wiki/Running-Tests)
|
109
|
+
- [Debugging Guide](https://github.com/itsthedevman/spec_forge/wiki/Debugging)
|
88
110
|
- [Dynamic Features](https://github.com/itsthedevman/spec_forge/wiki/Dynamic-Features)
|
89
111
|
- [Factory Support](https://github.com/itsthedevman/spec_forge/wiki/Factory-Support)
|
90
112
|
- [RSpec Matchers](https://github.com/itsthedevman/spec_forge/wiki/RSpec-Matchers)
|
91
113
|
|
92
114
|
Also see the [API Documentation](https://itsthedevman.com/docs/spec_forge).
|
93
115
|
|
94
|
-
##
|
95
|
-
|
96
|
-
Current development priorities:
|
97
|
-
- [ ] Negated matchers: `matcher.not`
|
98
|
-
- [ ] `create_list/build_list` factory strategies
|
99
|
-
- [ ] `transform.map` support
|
100
|
-
- [ ] XML/HTML response handling
|
101
|
-
- [ ] OpenAPI generation from tests
|
102
|
-
- [x] Array support for `json` expectations
|
103
|
-
- [x] Support for running individual specs
|
104
|
-
- [x] Improved error handling
|
116
|
+
## Future Development
|
105
117
|
|
106
|
-
Have a feature request? Open an issue on GitHub!
|
118
|
+
For the latest development priorities and feature ideas, check out our [Github Project](https://github.com/itsthedevman/spec_forge/projects?query=is%3Aopen). Have a feature request? Open an issue on GitHub!
|
107
119
|
|
108
120
|
## Contributing
|
109
121
|
|
data/flake.lock
CHANGED
@@ -20,11 +20,11 @@
|
|
20
20
|
},
|
21
21
|
"nixpkgs": {
|
22
22
|
"locked": {
|
23
|
-
"lastModified":
|
24
|
-
"narHash": "sha256-
|
23
|
+
"lastModified": 1742422364,
|
24
|
+
"narHash": "sha256-mNqIplmEohk5jRkqYqG19GA8MbQ/D4gQSK0Mu4LvfRQ=",
|
25
25
|
"owner": "NixOS",
|
26
26
|
"repo": "nixpkgs",
|
27
|
-
"rev": "
|
27
|
+
"rev": "a84ebe20c6bc2ecbcfb000a50776219f48d134cc",
|
28
28
|
"type": "github"
|
29
29
|
},
|
30
30
|
"original": {
|
data/flake.nix
CHANGED
@@ -6,8 +6,14 @@
|
|
6
6
|
flake-utils.url = "github:numtide/flake-utils";
|
7
7
|
};
|
8
8
|
|
9
|
-
outputs =
|
10
|
-
|
9
|
+
outputs =
|
10
|
+
{
|
11
|
+
self,
|
12
|
+
nixpkgs,
|
13
|
+
flake-utils,
|
14
|
+
}:
|
15
|
+
flake-utils.lib.eachDefaultSystem (
|
16
|
+
system:
|
11
17
|
let
|
12
18
|
pkgs = nixpkgs.legacyPackages.${system};
|
13
19
|
in
|
@@ -2,84 +2,272 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class Attribute
|
5
|
+
#
|
6
|
+
# Adds support for an attribute to accept n-number of chained calls. It supports chaining
|
7
|
+
# methods, hash keys, and array indexes. It also works well alongside Parameterized attributes
|
8
|
+
#
|
9
|
+
# This module requires being included into a class first before it can be used
|
10
|
+
#
|
11
|
+
# @example Basic usage in YAML
|
12
|
+
# my_variable: variable.users.first
|
13
|
+
#
|
14
|
+
# @example Advanced usage in YAML
|
15
|
+
# my_variable: variable.users.0.posts.second.author.name
|
16
|
+
#
|
17
|
+
# @example Basic usage in code
|
18
|
+
# faker = SpecForge::Attribute.from("faker.name.name.upcase")
|
19
|
+
# faker.resolved #=> BENDING UNIT 22
|
20
|
+
#
|
5
21
|
module Chainable
|
22
|
+
#
|
23
|
+
# Regular expression that matches pure numeric strings
|
24
|
+
# Used for detecting potential array index operations
|
25
|
+
#
|
26
|
+
# @return [Regexp] A case-insensitive regex matching strings containing only digits
|
27
|
+
#
|
6
28
|
NUMBER_REGEX = /^\d+$/i
|
7
29
|
|
8
|
-
|
30
|
+
#
|
31
|
+
# The first part of the chained attribute
|
32
|
+
#
|
33
|
+
# @return [Symbol] The first component of the chained attribute
|
34
|
+
#
|
35
|
+
attr_reader :keyword
|
36
|
+
|
37
|
+
#
|
38
|
+
# The second part of the chained attribute
|
39
|
+
#
|
40
|
+
# @return [Symbol] The second component of the chained attribute
|
41
|
+
#
|
42
|
+
attr_reader :header
|
43
|
+
|
44
|
+
#
|
45
|
+
# The remaining parts of the attribute chain after the header
|
46
|
+
#
|
47
|
+
# @return [Array<Symbol>] The remaining method/key invocations in the chain
|
48
|
+
#
|
49
|
+
attr_reader :invocation_chain
|
9
50
|
|
10
51
|
#
|
11
|
-
#
|
52
|
+
# The initial object from which the chain will start traversing
|
53
|
+
#
|
54
|
+
# @return [Object] The base object that starts the method/attribute chain
|
12
55
|
#
|
13
|
-
|
56
|
+
attr_reader :base_object
|
57
|
+
|
14
58
|
#
|
15
|
-
#
|
16
|
-
# Note: There can be any n number of segments.
|
59
|
+
# Initializes a new chainable attribute by parsing the input into components
|
17
60
|
#
|
18
61
|
def initialize(...)
|
19
62
|
super
|
20
63
|
|
21
|
-
|
22
|
-
sections = input.split(".")[1..]
|
64
|
+
sections = input.split(".")
|
23
65
|
|
24
|
-
@
|
25
|
-
@
|
66
|
+
@keyword = sections.first.to_sym
|
67
|
+
@header = sections.second&.to_sym
|
68
|
+
@invocation_chain = sections[2..] || []
|
26
69
|
end
|
27
70
|
|
71
|
+
#
|
72
|
+
# Returns the value of this attribute by resolving the chain
|
73
|
+
# Will return a new value on each call for dynamic attributes like Faker
|
74
|
+
#
|
75
|
+
# @return [Object] The result of invoking the chain on the base object
|
76
|
+
#
|
28
77
|
def value
|
29
78
|
invoke_chain
|
30
79
|
end
|
31
80
|
|
32
|
-
|
81
|
+
#
|
82
|
+
# Resolves the chain and stores the result
|
83
|
+
# The result is memoized, so subsequent calls return the same value
|
84
|
+
# even for dynamic attributes like Faker and Factory
|
85
|
+
#
|
86
|
+
# @return [Object] The fully resolved and memoized value
|
87
|
+
#
|
88
|
+
def resolved
|
33
89
|
@resolved ||= resolve_chain
|
34
90
|
end
|
35
91
|
|
36
92
|
private
|
37
93
|
|
94
|
+
#
|
95
|
+
# Invokes the chain by calling #value on each object
|
96
|
+
#
|
97
|
+
# @return [Object] The result of invoking the chain
|
98
|
+
#
|
99
|
+
# @private
|
100
|
+
#
|
38
101
|
def invoke_chain
|
39
102
|
traverse_chain(resolve: false)
|
40
103
|
end
|
41
104
|
|
105
|
+
#
|
106
|
+
# Resolves the chain by calling #resolve on each object
|
107
|
+
#
|
108
|
+
# @return [Object] The fully resolved result
|
109
|
+
#
|
110
|
+
# @private
|
111
|
+
#
|
42
112
|
def resolve_chain
|
43
|
-
|
113
|
+
traverse_chain(resolve: true)
|
44
114
|
end
|
45
115
|
|
116
|
+
#
|
117
|
+
# Traverses the chain of invocations step by step
|
118
|
+
#
|
119
|
+
# @param resolve [Boolean] Whether to use resolve during traversal
|
120
|
+
#
|
121
|
+
# @return [Object] The result of the traversal
|
122
|
+
#
|
123
|
+
# @private
|
124
|
+
#
|
46
125
|
def traverse_chain(resolve:)
|
47
|
-
|
48
|
-
|
126
|
+
resolution_path = {}
|
127
|
+
|
128
|
+
current_path = "#{keyword}.#{header}"
|
129
|
+
current_object = base_object
|
130
|
+
|
131
|
+
invocation_chain.each do |step|
|
132
|
+
next_value = retrieve_value(current_object, resolve:)
|
133
|
+
|
134
|
+
# Store this step's resolution for error reporting
|
135
|
+
resolution_path[current_path] = describe_value(next_value)
|
136
|
+
current_path += ".#{step}"
|
137
|
+
|
138
|
+
# Try to invoke the next step
|
139
|
+
current_object = invoke(step, next_value)
|
140
|
+
rescue Error::InvalidInvocationError => e
|
141
|
+
resolution_path[current_path] = "Error: #{e.message}"
|
49
142
|
|
50
|
-
|
143
|
+
raise e.with_resolution_path(resolution_path)
|
51
144
|
end
|
52
145
|
|
53
|
-
|
146
|
+
# Return final result
|
147
|
+
retrieve_value(current_object, resolve:)
|
54
148
|
end
|
55
149
|
|
150
|
+
#
|
151
|
+
# Retrieves the value from an object, resolving it if needed
|
152
|
+
#
|
153
|
+
# @param object [Object] The object to retrieve a value from
|
154
|
+
# @param resolve [Boolean] Whether to resolve the object's value
|
155
|
+
#
|
156
|
+
# @return [Object] The retrieved value
|
157
|
+
#
|
158
|
+
# @private
|
159
|
+
#
|
56
160
|
def retrieve_value(object, resolve:)
|
57
161
|
return object unless object.is_a?(Attribute)
|
58
162
|
|
59
|
-
resolve ? object.
|
163
|
+
resolve ? object.resolved : object.value
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
# Creates a description of a value for error messages
|
168
|
+
#
|
169
|
+
# @param value [Object] The value to describe
|
170
|
+
#
|
171
|
+
# @return [String] A description
|
172
|
+
#
|
173
|
+
# @private
|
174
|
+
#
|
175
|
+
def describe_value(value)
|
176
|
+
case value
|
177
|
+
when Context::Store::Entry
|
178
|
+
"Store with attributes: #{value.available_methods.join_map(", ", &:in_quotes)}"
|
179
|
+
when OpenStruct
|
180
|
+
"Object with attributes: #{value.table.keys.join_map(", ", &:in_quotes)}"
|
181
|
+
when Struct, Data
|
182
|
+
"Object with attributes: #{value.members.join_map(", ", &:in_quotes)}"
|
183
|
+
when ArrayLike
|
184
|
+
# Preview the first 5 value's classes
|
185
|
+
preview = value.take(5).map(&:class)
|
186
|
+
preview << "..." if value.size > 5
|
187
|
+
|
188
|
+
"Array with #{value.size} #{"element".pluralize(value.size)}: #{preview}"
|
189
|
+
when HashLike
|
190
|
+
# Preview the first 5 keys
|
191
|
+
keys = value.keys.take(5)
|
192
|
+
|
193
|
+
preview = keys.join_map(", ") { |key| "\"#{key}\"" }
|
194
|
+
preview += ", ..." if value.keys.size > 5
|
195
|
+
|
196
|
+
"Hash with #{"key".pluralize(keys.size)}: #{preview}"
|
197
|
+
when String
|
198
|
+
"\"#{value.truncate(50)}\""
|
199
|
+
when NilClass
|
200
|
+
"nil"
|
201
|
+
when Proc
|
202
|
+
"Proc defined at #{value.source_location.join(":")}"
|
203
|
+
else
|
204
|
+
"#{value.class}: #{value.inspect[0..50]}"
|
205
|
+
end
|
60
206
|
end
|
61
207
|
|
208
|
+
#
|
209
|
+
# Invokes an operation on an object based on the step type (hash key, array index, or method)
|
210
|
+
#
|
211
|
+
# @param step [String] The step to invoke
|
212
|
+
# @param object [Object] The object to invoke the step on
|
213
|
+
#
|
214
|
+
# @return [Object] The result of the invocation
|
215
|
+
#
|
216
|
+
# @raise [Error::InvalidInvocationError] If the step cannot be invoked on the object
|
217
|
+
#
|
218
|
+
# @private
|
219
|
+
#
|
62
220
|
def invoke(step, object)
|
63
221
|
if hash_key?(object, step)
|
64
|
-
object[step.to_sym]
|
222
|
+
object[step.to_s] || object[step.to_sym]
|
65
223
|
elsif index?(object, step)
|
66
224
|
object[step.to_i]
|
67
225
|
elsif method?(object, step)
|
68
226
|
object.public_send(step)
|
69
227
|
else
|
70
|
-
raise InvalidInvocationError.new(step, object)
|
228
|
+
raise Error::InvalidInvocationError.new(step, object)
|
71
229
|
end
|
72
230
|
end
|
73
231
|
|
232
|
+
#
|
233
|
+
# Checks if the object can be accessed with the given key
|
234
|
+
#
|
235
|
+
# @param object [Object] The object to check
|
236
|
+
# @param key [String] The key to check
|
237
|
+
#
|
238
|
+
# @return [Boolean] Whether the object supports hash-like access with the key
|
239
|
+
#
|
240
|
+
# @private
|
241
|
+
#
|
74
242
|
def hash_key?(object, key)
|
75
|
-
# This is to support the silly delegator
|
76
|
-
method?(object, :key?) && object.key?(key.to_sym)
|
243
|
+
# This is to support the silly delegator and both symbol/string
|
244
|
+
method?(object, :key?) && (object.key?(key.to_s) || object.key?(key.to_sym))
|
77
245
|
end
|
78
246
|
|
247
|
+
#
|
248
|
+
# Checks if the object responds to the given method
|
249
|
+
#
|
250
|
+
# @param object [Object] The object to check
|
251
|
+
# @param method_name [String, Symbol] The method name to check
|
252
|
+
#
|
253
|
+
# @return [Boolean] Whether the object responds to the method
|
254
|
+
#
|
255
|
+
# @private
|
256
|
+
#
|
79
257
|
def method?(object, method_name)
|
80
258
|
object.respond_to?(method_name)
|
81
259
|
end
|
82
260
|
|
261
|
+
#
|
262
|
+
# Checks if the object supports array-like access with the given index
|
263
|
+
#
|
264
|
+
# @param object [Object] The object to check
|
265
|
+
# @param step [String] The potential index
|
266
|
+
#
|
267
|
+
# @return [Boolean] Whether the object supports array-like access with the step
|
268
|
+
#
|
269
|
+
# @private
|
270
|
+
#
|
83
271
|
def index?(object, step)
|
84
272
|
# This is to support the silly delegator
|
85
273
|
method?(object, :index) && step.match?(NUMBER_REGEX)
|