graph_weaver 0.0.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/Gemfile.lock +45 -17
- data/Makefile +6 -1
- data/PLAN.md +52 -22
- data/README.md +62 -17
- data/docs/errors.md +106 -0
- data/docs/generated_modules.md +119 -0
- data/docs/real_world.md +46 -0
- data/docs/scalars.md +69 -0
- data/docs/testing.md +93 -0
- data/graph_weaver.gemspec +3 -0
- data/lib/graph_weaver/codegen/emit.rb +237 -0
- data/lib/graph_weaver/codegen/nodes.rb +256 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +238 -0
- data/lib/graph_weaver/codegen.rb +156 -395
- data/lib/graph_weaver/errors.rb +310 -0
- data/lib/graph_weaver/faraday_executor.rb +61 -0
- data/lib/graph_weaver/http_executor.rb +16 -3
- data/lib/graph_weaver/inflect.rb +18 -0
- data/lib/graph_weaver/response.rb +55 -0
- data/lib/graph_weaver/rspec.rb +54 -0
- data/lib/graph_weaver/schema_loader.rb +73 -9
- data/lib/graph_weaver/selection.rb +68 -0
- data/lib/graph_weaver/testing/cassette.rb +203 -0
- data/lib/graph_weaver/testing/failure.rb +109 -0
- data/lib/graph_weaver/testing/fake_executor.rb +228 -0
- data/lib/graph_weaver/testing/rspec.rb +5 -0
- data/lib/graph_weaver/testing/values.rb +98 -0
- data/lib/graph_weaver/testing.rb +90 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +97 -3
- metadata +63 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
# The value engine behind FakeExecutor and Cassette#anonymize!: seeded,
|
|
7
|
+
# type-correct scalar generation with optional faker-backed semantics
|
|
8
|
+
# matched on field names — strings (name/email/url/...) and numbers
|
|
9
|
+
# (age/price/count/latitude/...) alike. Keeps a consistent id mapping so
|
|
10
|
+
# the same original id always anonymizes to the same fake id.
|
|
11
|
+
class GraphWeaver::Testing::Values
|
|
12
|
+
include GraphWeaver::Inflect
|
|
13
|
+
|
|
14
|
+
STRING_SEMANTICS = {
|
|
15
|
+
/email/ => -> { ::Faker::Internet.email },
|
|
16
|
+
/(^|_)first_name$/ => -> { ::Faker::Name.first_name },
|
|
17
|
+
/(^|_)last_name$/ => -> { ::Faker::Name.last_name },
|
|
18
|
+
/(^|_)(full_)?name$/ => -> { ::Faker::Name.name },
|
|
19
|
+
/(^|_)(url|website|link)$/ => -> { ::Faker::Internet.url },
|
|
20
|
+
/phone/ => -> { ::Faker::PhoneNumber.phone_number },
|
|
21
|
+
/(^|_)address$/ => -> { ::Faker::Address.full_address },
|
|
22
|
+
/(^|_)(city)$/ => -> { ::Faker::Address.city },
|
|
23
|
+
/(^|_)(title|description)$/ => -> { ::Faker::Lorem.sentence(word_count: 3) },
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
NUMBER_SEMANTICS = {
|
|
27
|
+
/(^|_)age$/ => ->(rng) { rng.rand(1..99) },
|
|
28
|
+
/(^|_)(price|amount|cost|total)(_cents)?$/ => ->(rng) { (rng.rand(1.0..10_000.0) * 100).round / 100.0 },
|
|
29
|
+
/(^|_)(count|quantity|size)$/ => ->(rng) { rng.rand(0..100) },
|
|
30
|
+
/latitude/ => ->(rng) { rng.rand(-90.0..90.0).round(6) },
|
|
31
|
+
/longitude/ => ->(rng) { rng.rand(-180.0..180.0).round(6) },
|
|
32
|
+
/(^|_)year$/ => ->(rng) { rng.rand(1970..2030) },
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
attr_reader :rng
|
|
36
|
+
|
|
37
|
+
def initialize(seed: nil, mode: nil)
|
|
38
|
+
config = GraphWeaver::Testing.config
|
|
39
|
+
@rng = Random.new(seed || config.seed || Random.new_seed)
|
|
40
|
+
@mode = resolve_mode(mode || config.mode)
|
|
41
|
+
@sequence = 0
|
|
42
|
+
@id_map = {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def scalar(type_name, field_name)
|
|
46
|
+
prop = underscore(field_name)
|
|
47
|
+
|
|
48
|
+
if @mode == :faker
|
|
49
|
+
# rebind per call: several Values instances may interleave (e.g. two
|
|
50
|
+
# seeded executors), and faker's rng is global
|
|
51
|
+
::Faker::Config.random = @rng
|
|
52
|
+
case type_name
|
|
53
|
+
when "String"
|
|
54
|
+
STRING_SEMANTICS.each { |pattern, faker| return faker.call if pattern.match?(prop) }
|
|
55
|
+
when "Int", "Float"
|
|
56
|
+
NUMBER_SEMANTICS.each do |pattern, gen|
|
|
57
|
+
next unless pattern.match?(prop)
|
|
58
|
+
|
|
59
|
+
value = gen.call(@rng)
|
|
60
|
+
return type_name == "Int" ? value.to_i : value.to_f
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
case type_name
|
|
66
|
+
when "ID" then (@sequence += 1).to_s
|
|
67
|
+
when "String" then "#{field_name}-#{@sequence += 1}"
|
|
68
|
+
when "Int" then @rng.rand(0..1_000)
|
|
69
|
+
when "Float" then @rng.rand(0.0..1_000.0).round(2)
|
|
70
|
+
when "Boolean" then [true, false].sample(random: @rng)
|
|
71
|
+
when "Date" then (Date.new(2020, 1, 1) + @rng.rand(0..2_000)).iso8601
|
|
72
|
+
when "DateTime", "Time", "ISO8601DateTime" then Time.at(1_600_000_000 + @rng.rand(0..100_000_000)).utc.iso8601
|
|
73
|
+
else "#{type_name}-#{@sequence += 1}" # unknown custom scalar: override it
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# same original id => same fake id, so relationships survive anonymization
|
|
78
|
+
def mapped_id(original)
|
|
79
|
+
@id_map[original] ||= (@sequence += 1).to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# :faker is an explicit ask — fail loudly when the gem is missing; auto
|
|
85
|
+
# (nil) quietly falls back to :literal
|
|
86
|
+
def resolve_mode(mode)
|
|
87
|
+
case mode
|
|
88
|
+
when :faker
|
|
89
|
+
raise ArgumentError, "mode: :faker requires the faker gem (add it to your Gemfile's test group)" unless defined?(::Faker)
|
|
90
|
+
|
|
91
|
+
:faker
|
|
92
|
+
when :literal then :literal
|
|
93
|
+
when nil then defined?(::Faker) ? :faker : :literal
|
|
94
|
+
else
|
|
95
|
+
raise ArgumentError, "mode: must be one of #{GraphWeaver::Testing::MODES.inspect} (or nil for auto), got #{mode.inspect}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../graph_weaver"
|
|
5
|
+
|
|
6
|
+
# faker is optional — semantic values (name/email/age/price/...) when
|
|
7
|
+
# present, type-based values when not
|
|
8
|
+
begin
|
|
9
|
+
require "faker"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
# fall back to type-based generation
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Opt-in test tooling: require "graph_weaver/testing" from your spec
|
|
15
|
+
# helper (never from production code). Configure once, initializer-style:
|
|
16
|
+
#
|
|
17
|
+
# GraphWeaver::Testing.configure do |config|
|
|
18
|
+
# config.schema = MySchema # for auto_fake / cassettes
|
|
19
|
+
# config.seed = 42 # reproducible fakes
|
|
20
|
+
# config.mode = :faker # or :literal; nil = auto
|
|
21
|
+
# config.overrides = { "Person.name" => "Daniel" }
|
|
22
|
+
# config.list_size = 2..4
|
|
23
|
+
# config.null_chance = 0.1 # nullable fields go nil sometimes
|
|
24
|
+
# config.cassette_dir = "spec/cassettes"
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# mode picks how values are fabricated:
|
|
28
|
+
# :faker — semantic, field-name matched (requires the faker gem)
|
|
29
|
+
# :literal — plain type-derived values ("name-1", seeded numbers)
|
|
30
|
+
# nil — auto: :faker when the gem is loaded, else :literal
|
|
31
|
+
#
|
|
32
|
+
# rspec users: require "graph_weaver/rspec" instead — it hooks the suite
|
|
33
|
+
# (seed from rspec, optional auto-faked executor per example).
|
|
34
|
+
module GraphWeaver
|
|
35
|
+
module Testing
|
|
36
|
+
MODES = [:faker, :literal].freeze
|
|
37
|
+
|
|
38
|
+
class Config
|
|
39
|
+
attr_accessor :overrides, :seed, :list_size, :null_chance, :schema, :cassette_dir, :auto_fake
|
|
40
|
+
attr_reader :mode
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@overrides = {}
|
|
44
|
+
@seed = nil
|
|
45
|
+
@list_size = 1..3
|
|
46
|
+
@null_chance = 0.0
|
|
47
|
+
@mode = nil # auto
|
|
48
|
+
@schema = nil
|
|
49
|
+
@cassette_dir = "spec/cassettes"
|
|
50
|
+
@auto_fake = false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def mode=(mode)
|
|
54
|
+
unless mode.nil? || MODES.include?(mode)
|
|
55
|
+
raise ArgumentError, "mode: must be one of #{MODES.inspect} (or nil for auto), got #{mode.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@mode = mode
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
def config
|
|
64
|
+
@config ||= Config.new
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def configure
|
|
68
|
+
yield config
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# back to defaults — between tests, or to undo an experiment
|
|
72
|
+
def reset!
|
|
73
|
+
@config = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# resolve a cassette name ("github") against cassette_dir; paths
|
|
77
|
+
# with separators or extensions pass through
|
|
78
|
+
def cassette_path(name)
|
|
79
|
+
return name if name.include?("/") || name.end_with?(".yml", ".yaml")
|
|
80
|
+
|
|
81
|
+
File.join(config.cassette_dir, "#{name}.yml")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
require_relative "testing/values"
|
|
88
|
+
require_relative "testing/fake_executor"
|
|
89
|
+
require_relative "testing/failure"
|
|
90
|
+
require_relative "testing/cassette"
|
data/lib/graph_weaver/version.rb
CHANGED
data/lib/graph_weaver.rb
CHANGED
|
@@ -1,11 +1,105 @@
|
|
|
1
1
|
require "graphql"
|
|
2
2
|
require "sorbet-runtime"
|
|
3
3
|
|
|
4
|
+
require_relative "graph_weaver/errors"
|
|
5
|
+
require_relative "graph_weaver/response"
|
|
6
|
+
require_relative "graph_weaver/inflect"
|
|
4
7
|
require_relative "graph_weaver/codegen"
|
|
5
8
|
require_relative "graph_weaver/http_executor"
|
|
6
9
|
require_relative "graph_weaver/schema_loader"
|
|
7
10
|
require_relative "graph_weaver/version"
|
|
8
11
|
|
|
9
|
-
# opt-in:
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
+
# opt-in extras:
|
|
13
|
+
# require "graph_weaver/faraday_executor" # Faraday transport
|
|
14
|
+
# require "graph_weaver/directive_defaults_patch" # fix graphql-ruby
|
|
15
|
+
# dropping directive argument defaults when loading SDL (needed for
|
|
16
|
+
# Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)
|
|
17
|
+
module GraphWeaver
|
|
18
|
+
class << self
|
|
19
|
+
# global default transport; generated modules fall back to this
|
|
20
|
+
# (override per module with MyQuery.executor=, or per call with
|
|
21
|
+
# execute(executor:))
|
|
22
|
+
attr_writer :executor
|
|
23
|
+
|
|
24
|
+
def executor
|
|
25
|
+
@executor or raise Error, "no executor configured — set GraphWeaver.executor= or pass executor:"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Teach the generator how a GraphQL custom scalar deserializes into a
|
|
29
|
+
# rich Ruby object (and serializes back onto the wire when used as a
|
|
30
|
+
# variable):
|
|
31
|
+
#
|
|
32
|
+
# GraphWeaver.register_scalar("Money", type: Money, requires: "bigdecimal")
|
|
33
|
+
#
|
|
34
|
+
# A field typed `Money` then generates `const :price, T.nilable(Money)`
|
|
35
|
+
# and casts with `Money.parse(...)` in from_h. Pass a real class as
|
|
36
|
+
# type: and cast:/serialize: are inferred from it — .parse/#to_s, or
|
|
37
|
+
# .load/.dump — by probing the deserialize side (see ScalarType::CODECS).
|
|
38
|
+
# Override with a Symbol method name (safest — no string to misspell), a
|
|
39
|
+
# Proc(expr) => code string, or :itself to force pass-through. requires:
|
|
40
|
+
# (a String or Array) names files the generated code needs — validated,
|
|
41
|
+
# and actually required to confirm it resolves when type: is a real class.
|
|
42
|
+
# coerce: true makes a variable of this scalar accept the value OR its
|
|
43
|
+
# raw input (e.g. "12.00"), running the latter through the cast before
|
|
44
|
+
# serializing — it raises on bad input, so some safety survives. Built-in
|
|
45
|
+
# scalars are pre-registered the same way, so this also overrides them.
|
|
46
|
+
# Call before generating.
|
|
47
|
+
def register_scalar(graphql_name, type:, cast: nil, serialize: nil, requires: nil, coerce: false)
|
|
48
|
+
Codegen.register_scalar(graphql_name, type:, cast:, serialize:, requires:, coerce:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Restore the built-in scalars, dropping every custom registration —
|
|
52
|
+
# the clean slate to reach for between tests or to undo overrides. Pass
|
|
53
|
+
# coerce: true to reload the built-ins with input coercion enabled
|
|
54
|
+
# (Float accepts 5/"5", etc.), then register your own scalars on top.
|
|
55
|
+
def reset_scalars!(coerce: false)
|
|
56
|
+
Codegen.reset_scalars!(coerce:)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Empty the scalar registry entirely, built-ins included (see
|
|
60
|
+
# reset_scalars! to restore the defaults).
|
|
61
|
+
def clear_scalars!
|
|
62
|
+
Codegen.clear_scalars!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parse a query into a typed query module:
|
|
66
|
+
#
|
|
67
|
+
# PersonQuery = GraphWeaver.parse(schema:, query: "queries/person.graphql")
|
|
68
|
+
#
|
|
69
|
+
# query is a .graphql/.gql path (module name derived from the file
|
|
70
|
+
# name) or a raw query string (name derived from the operation name,
|
|
71
|
+
# falling back to "Query" for anonymous operations — collisions are
|
|
72
|
+
# impossible since each parse gets its own container). Pass name: to
|
|
73
|
+
# override, executor: to set the module's transport.
|
|
74
|
+
def parse(schema:, query:, name: nil, executor: nil)
|
|
75
|
+
if query.end_with?(".graphql", ".gql")
|
|
76
|
+
name ||= "#{Inflect.camelize(File.basename(query, ".*"))}Query"
|
|
77
|
+
query = File.read(query)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
Codegen.parse(schema:, query:, module_name: name, executor:)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# One-shot dynamic execution — no module handling, no build step:
|
|
84
|
+
#
|
|
85
|
+
# GraphWeaver.execute(schema:, query:, variables: { id: "1" }) # => Response
|
|
86
|
+
# GraphWeaver.execute!(schema:, query:, variables: { id: "1" }) # => Result (or raise)
|
|
87
|
+
#
|
|
88
|
+
# Mirrors a generated module: execute returns the Response envelope,
|
|
89
|
+
# execute! returns the typed result directly and raises QueryError on
|
|
90
|
+
# top-level errors. Transport precedence: executor: param, then
|
|
91
|
+
# GraphWeaver.executor, then in-process execution against schema.
|
|
92
|
+
# Variable keys may be graphql-cased strings or ruby symbols.
|
|
93
|
+
def execute(schema:, query:, variables: {}, executor: nil)
|
|
94
|
+
executor ||= @executor || schema
|
|
95
|
+
mod = parse(schema:, query:, executor:)
|
|
96
|
+
kwargs = variables.to_h { |key, value| [Inflect.underscore(key.to_s).to_sym, value] }
|
|
97
|
+
mod.execute(**kwargs)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# execute + data! — the typed result, or a raised QueryError. See execute.
|
|
101
|
+
def execute!(schema:, query:, variables: {}, executor: nil)
|
|
102
|
+
execute(schema:, query:, variables:, executor:).data!
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graph_weaver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bigdecimal
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: debug
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +65,34 @@ dependencies:
|
|
|
51
65
|
- - ">="
|
|
52
66
|
- !ruby/object:Gem::Version
|
|
53
67
|
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: faker
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: faraday
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
54
96
|
- !ruby/object:Gem::Dependency
|
|
55
97
|
name: rspec
|
|
56
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -135,12 +177,32 @@ files:
|
|
|
135
177
|
- NOTES.md
|
|
136
178
|
- PLAN.md
|
|
137
179
|
- README.md
|
|
180
|
+
- docs/errors.md
|
|
181
|
+
- docs/generated_modules.md
|
|
182
|
+
- docs/real_world.md
|
|
183
|
+
- docs/scalars.md
|
|
184
|
+
- docs/testing.md
|
|
138
185
|
- graph_weaver.gemspec
|
|
139
186
|
- lib/graph_weaver.rb
|
|
140
187
|
- lib/graph_weaver/codegen.rb
|
|
188
|
+
- lib/graph_weaver/codegen/emit.rb
|
|
189
|
+
- lib/graph_weaver/codegen/nodes.rb
|
|
190
|
+
- lib/graph_weaver/codegen/scalar_type.rb
|
|
141
191
|
- lib/graph_weaver/directive_defaults_patch.rb
|
|
192
|
+
- lib/graph_weaver/errors.rb
|
|
193
|
+
- lib/graph_weaver/faraday_executor.rb
|
|
142
194
|
- lib/graph_weaver/http_executor.rb
|
|
195
|
+
- lib/graph_weaver/inflect.rb
|
|
196
|
+
- lib/graph_weaver/response.rb
|
|
197
|
+
- lib/graph_weaver/rspec.rb
|
|
143
198
|
- lib/graph_weaver/schema_loader.rb
|
|
199
|
+
- lib/graph_weaver/selection.rb
|
|
200
|
+
- lib/graph_weaver/testing.rb
|
|
201
|
+
- lib/graph_weaver/testing/cassette.rb
|
|
202
|
+
- lib/graph_weaver/testing/failure.rb
|
|
203
|
+
- lib/graph_weaver/testing/fake_executor.rb
|
|
204
|
+
- lib/graph_weaver/testing/rspec.rb
|
|
205
|
+
- lib/graph_weaver/testing/values.rb
|
|
144
206
|
- lib/graph_weaver/version.rb
|
|
145
207
|
homepage: https://github.com/dpep/graph_weaver
|
|
146
208
|
licenses:
|