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.
@@ -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"
@@ -1,3 +1,3 @@
1
1
  module GraphWeaver
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
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: require "graph_weaver/directive_defaults_patch" to fix
10
- # graphql-ruby dropping directive argument defaults when loading SDL
11
- # (needed for Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)
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.1
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: