Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow accessing enum value names via methods #5206

Merged
merged 8 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gemfiles/mongoid_7.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ if RUBY_VERSION >= "3.0"
gem "evt"
end
gem "fiber-storage"
gem "concurrent-ruby", "1.3.4"

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/rails_7.0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ gem "sqlite3", "~> 1.4", platform: :ruby
gem "sequel"
gem "evt"
gem "async"
gem "concurrent-ruby", "1.3.4"

gemspec path: "../"
22 changes: 22 additions & 0 deletions guides/type_definitions/enums.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,25 @@ value "AUDIO", value: :audio
Then, GraphQL inputs of `AUDIO` will be converted to `:audio` and Ruby values of `:audio` will be converted to `"AUDIO"` in GraphQL responses.

Enum classes are never instantiated and their methods are never called.

You can get the GraphQL name of the enum value using the method matching its downcased name:

```ruby
Types::MediaCategory.audio # => "AUDIO"
```

You can pass a `value_method:` to override the value of the generated method:

```ruby
value "AUDIO", value: :audio, value_method: :lo_fi_audio

# ...

Types::MediaCategory.lo_fi_audio # => "AUDIO"
```

Also, you can completely skip the method generation by setting `value_method` to `false`

```ruby
value "AUDIO", value: :audio, value_method: false
```
2 changes: 1 addition & 1 deletion lib/graphql/introspection/directive_location_enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class DirectiveLocationEnum < GraphQL::Schema::Enum
"a __DirectiveLocation describes one such possible adjacencies."

GraphQL::Schema::Directive::LOCATIONS.each do |location|
value(location.to_s, GraphQL::Schema::Directive::LOCATION_DESCRIPTIONS[location], value: location)
value(location.to_s, GraphQL::Schema::Directive::LOCATION_DESCRIPTIONS[location], value: location, value_method: false)
end
introspection true
end
Expand Down
20 changes: 19 additions & 1 deletion lib/graphql/schema/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,16 @@ class << self
# @option kwargs [String] :description, the GraphQL description for this value, present in documentation
# @option kwargs [String] :comment, the GraphQL comment for this value, present in documentation
# @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`)
# @option kwargs [::Object] :value_method, the method name to fetch `graphql_name` (defaults to `graphql_name.downcase`)
# @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here
# @return [void]
# @see {Schema::EnumValue} which handles these inputs by default
def value(*args, **kwargs, &block)
def value(*args, value_method: nil, **kwargs, &block)
kwargs[:owner] = self
value = enum_value_class.new(*args, **kwargs, &block)

generate_value_method(value, value_method)

key = value.graphql_name
prev_value = own_values[key]
case prev_value
Expand Down Expand Up @@ -223,6 +227,20 @@ def inherited(child_class)
def own_values
@own_values ||= {}
end

def generate_value_method(value, configured_value_method)
return if configured_value_method == false

value_method_name = configured_value_method || value.graphql_name.downcase

if respond_to?(value_method_name.to_sym)
warn "Failed to define value method for :#{value_method_name}, because " \
"#{value.owner.name} already responds to that method. Use `value_name:` to override the method name."
return
end

instance_eval("def #{value_method_name}; #{value.graphql_name.inspect}; end;")
end
end

enum_value_class(GraphQL::Schema::EnumValue)
Expand Down
39 changes: 39 additions & 0 deletions spec/graphql/schema/enum_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,45 @@
end
end

describe "value methods" do
it "defines default methods to fetch graphql names" do
assert_equal enum.string, "STRING"
assert_equal enum.woodwind, "WOODWIND"
assert_equal enum.brass, "BRASS"
assert_equal enum.didgeridoo, "DIDGERIDOO"
assert_equal enum.keys, "KEYS"
end

describe "when value_method is configured" do
it "use custom method" do
assert_equal enum.respond_to?(:percussion), false
assert_equal enum.precussion_custom_value_method, "PERCUSSION"
end
end

describe "when value_method conflicts with existing method" do
it "does not define method and emits warning" do
expected_message = "Failed to define value method for :value, because " \
"ConflictEnum already responds to that method. Use `value_name:` to override the method name.\n"

assert_warns(expected_message) do
conflict_enum = Class.new(GraphQL::Schema::Enum)
Object.const_set("ConflictEnum", conflict_enum)
already_defined_method = conflict_enum.method(:value)
conflict_enum.value "VALUE", "Makes conflict"
assert_equal conflict_enum.method(:value), already_defined_method
end
end

end

describe "when value_method = false" do
it "does not define method" do
assert_equal enum.respond_to?(:silence), false
end
end
end

describe "type info" do
it "tells about the definition" do
assert_equal "Family", enum.graphql_name
Expand Down
5 changes: 3 additions & 2 deletions spec/support/jazz.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,13 @@ class Family < BaseEnum
value "STRING", "Makes a sound by vibrating strings", value: :str, custom_setting: 1
value :WOODWIND, "Makes a sound by vibrating air in a pipe"
value :BRASS, "Makes a sound by amplifying the sound of buzzing lips"
value "PERCUSSION", "Makes a sound by hitting something that vibrates"
value "PERCUSSION", "Makes a sound by hitting something that vibrates",
value_method: :precussion_custom_value_method
value "DIDGERIDOO", "Makes a sound by amplifying the sound of buzzing lips", deprecation_reason: "Merged into BRASS"
value "KEYS" do
description "Neither here nor there, really"
end
value "SILENCE", "Makes no sound", value: false
value "SILENCE", "Makes no sound", value: false, value_method: false
end

class InstrumentType < BaseObject
Expand Down
Loading