summaryrefslogtreecommitdiff
path: root/app/graphql
diff options
context:
space:
mode:
authorBob Van Landuyt <bob@vanlanduyt.co>2018-05-23 09:55:14 +0200
committerBob Van Landuyt <bob@vanlanduyt.co>2018-06-06 10:58:54 +0200
commit9b65d4bb417fb4939289eab94487c894f0a62db6 (patch)
tree1f97b9a1bd0d722a3c3ff4e89ec13bdb7a3aec00 /app/graphql
parentc443133e779c4c508b9c6429dd4ba623d64f03f1 (diff)
downloadgitlab-ce-9b65d4bb417fb4939289eab94487c894f0a62db6.tar.gz
Initial setup GraphQL using graphql-ruby 1.8
- All definitions have been replaced by classes: http://graphql-ruby.org/schema/class_based_api.html - Authorization & Presentation have been refactored to work in the class based system - Loaders have been replaced by resolvers - Times are now coersed as ISO 8601
Diffstat (limited to 'app/graphql')
-rw-r--r--app/graphql/functions/base_function.rb4
-rw-r--r--app/graphql/functions/echo.rb13
-rw-r--r--app/graphql/gitlab_schema.rb12
-rw-r--r--app/graphql/loaders/base_loader.rb15
-rw-r--r--app/graphql/loaders/iid_loader.rb23
-rw-r--r--app/graphql/resolvers/base_resolver.rb4
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb (renamed from app/graphql/loaders/full_path_loader.rb)16
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb21
-rw-r--r--app/graphql/resolvers/project_resolver.rb11
-rw-r--r--app/graphql/types/base_enum.rb4
-rw-r--r--app/graphql/types/base_field.rb5
-rw-r--r--app/graphql/types/base_input_object.rb4
-rw-r--r--app/graphql/types/base_interface.rb5
-rw-r--r--app/graphql/types/base_object.rb7
-rw-r--r--app/graphql/types/base_scalar.rb4
-rw-r--r--app/graphql/types/base_union.rb4
-rw-r--r--app/graphql/types/merge_request_type.rb87
-rw-r--r--app/graphql/types/mutation_type.rb8
-rw-r--r--app/graphql/types/project_type.rb88
-rw-r--r--app/graphql/types/query_type.rb47
-rw-r--r--app/graphql/types/time_type.rb18
21 files changed, 218 insertions, 182 deletions
diff --git a/app/graphql/functions/base_function.rb b/app/graphql/functions/base_function.rb
new file mode 100644
index 00000000000..42fb8f99acc
--- /dev/null
+++ b/app/graphql/functions/base_function.rb
@@ -0,0 +1,4 @@
+module Functions
+ class BaseFunction < GraphQL::Function
+ end
+end
diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb
new file mode 100644
index 00000000000..e5bf109b8d7
--- /dev/null
+++ b/app/graphql/functions/echo.rb
@@ -0,0 +1,13 @@
+module Functions
+ class Echo < BaseFunction
+ argument :text, GraphQL::STRING_TYPE
+
+ description "Testing endpoint to validate the API with"
+
+ def call(obj, args, ctx)
+ username = ctx[:current_user]&.username
+
+ "#{username.inspect} says: #{args[:text]}"
+ end
+ end
+end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index ac75c4bf2e3..de4fc1d8e32 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -1,12 +1,8 @@
-Gitlab::Graphql::Authorize.register!
-Gitlab::Graphql::Present.register!
-
-GitlabSchema = GraphQL::Schema.define do
+class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL
-
- enable_preloading
- enable_authorization
- enable_presenting
+ use Gitlab::Graphql::Authorize
+ use Gitlab::Graphql::Present
query(Types::QueryType)
+ # mutation(Types::MutationType)
end
diff --git a/app/graphql/loaders/base_loader.rb b/app/graphql/loaders/base_loader.rb
deleted file mode 100644
index aad435ea09b..00000000000
--- a/app/graphql/loaders/base_loader.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# Helper methods for all loaders
-module Loaders::BaseLoader
- extend ActiveSupport::Concern
-
- class_methods do
- # Convert a class method into a resolver proc. The method should follow the
- # (obj, args, ctx) calling convention
- def [](sym)
- resolver = method(sym)
- raise ArgumentError.new("#{self}.#{sym} is not a resolver") unless resolver.arity == 3
-
- resolver
- end
- end
-end
diff --git a/app/graphql/loaders/iid_loader.rb b/app/graphql/loaders/iid_loader.rb
deleted file mode 100644
index f9827765a92..00000000000
--- a/app/graphql/loaders/iid_loader.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class Loaders::IidLoader
- include Loaders::BaseLoader
-
- class << self
- def merge_request(obj, args, ctx)
- iid = args[:iid]
- project = Loaders::FullPathLoader.project_by_full_path(args[:project])
- merge_request_by_project_and_iid(project, iid)
- end
-
- def merge_request_by_project_and_iid(project_loader, iid)
- project_id = project_loader.__sync&.id
-
- # IIDs are represented as the GraphQL `id` type, which is a string
- BatchLoader.for(iid.to_s).batch(key: "merge_request:target_project:#{project_id}:iid") do |iids, loader|
- if project_id
- results = MergeRequest.where(target_project_id: project_id, iid: iids)
- results.each { |mr| loader.call(mr.iid.to_s, mr) }
- end
- end
- end
- end
-end
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
new file mode 100644
index 00000000000..89b7f9dad6f
--- /dev/null
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -0,0 +1,4 @@
+module Resolvers
+ class BaseResolver < GraphQL::Schema::Resolver
+ end
+end
diff --git a/app/graphql/loaders/full_path_loader.rb b/app/graphql/resolvers/full_path_resolver.rb
index 27ada90b82a..4eb28aaed6c 100644
--- a/app/graphql/loaders/full_path_loader.rb
+++ b/app/graphql/resolvers/full_path_resolver.rb
@@ -1,13 +1,11 @@
-module Loaders::FullPathLoader
- include Loaders::BaseLoader
+module Resolvers
+ module FullPathResolver
+ extend ActiveSupport::Concern
- class << self
- def project(obj, args, ctx)
- project_by_full_path(args[:full_path])
- end
-
- def project_by_full_path(full_path)
- model_by_full_path(Project, full_path)
+ prepended do
+ argument :full_path, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"'
end
def model_by_full_path(model, full_path)
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
new file mode 100644
index 00000000000..b1857ab09f7
--- /dev/null
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -0,0 +1,21 @@
+module Resolvers
+ class MergeRequestResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::ProjectType, null: true
+
+ argument :iid, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The IID of the merge request, e.g., "1"'
+
+ def resolve(full_path:, iid:)
+ project = model_by_full_path(Project, full_path)
+ return unless project.present?
+
+ BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
+ results = project.merge_requests.where(iid: iids)
+ results.each { |mr| loader.call(mr.iid.to_s, mr) }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb
new file mode 100644
index 00000000000..ec115bad896
--- /dev/null
+++ b/app/graphql/resolvers/project_resolver.rb
@@ -0,0 +1,11 @@
+module Resolvers
+ class ProjectResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::ProjectType, null: true
+
+ def resolve(full_path:)
+ model_by_full_path(Project, full_path)
+ end
+ end
+end
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
new file mode 100644
index 00000000000..b45a845f74f
--- /dev/null
+++ b/app/graphql/types/base_enum.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseEnum < GraphQL::Schema::Enum
+ end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
new file mode 100644
index 00000000000..c5740a334d7
--- /dev/null
+++ b/app/graphql/types/base_field.rb
@@ -0,0 +1,5 @@
+module Types
+ class BaseField < GraphQL::Schema::Field
+ prepend Gitlab::Graphql::Authorize
+ end
+end
diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb
new file mode 100644
index 00000000000..309e336e6c8
--- /dev/null
+++ b/app/graphql/types/base_input_object.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseInputObject < GraphQL::Schema::InputObject
+ end
+end
diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb
new file mode 100644
index 00000000000..69e72dc5808
--- /dev/null
+++ b/app/graphql/types/base_interface.rb
@@ -0,0 +1,5 @@
+module Types
+ module BaseInterface
+ include GraphQL::Schema::Interface
+ end
+end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
new file mode 100644
index 00000000000..e033ef96ce9
--- /dev/null
+++ b/app/graphql/types/base_object.rb
@@ -0,0 +1,7 @@
+module Types
+ class BaseObject < GraphQL::Schema::Object
+ prepend Gitlab::Graphql::Present
+
+ field_class Types::BaseField
+ end
+end
diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb
new file mode 100644
index 00000000000..c0aa38be239
--- /dev/null
+++ b/app/graphql/types/base_scalar.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseScalar < GraphQL::Schema::Scalar
+ end
+end
diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb
new file mode 100644
index 00000000000..36337fc6ee5
--- /dev/null
+++ b/app/graphql/types/base_union.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseUnion < GraphQL::Schema::Union
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index fc430ca03d5..d5d24952984 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -1,46 +1,47 @@
-Types::MergeRequestType = GraphQL::ObjectType.define do
- present_using MergeRequestPresenter
+module Types
+ class MergeRequestType < BaseObject
+ present_using MergeRequestPresenter
- name 'MergeRequest'
+ graphql_name 'MergeRequest'
- field :id, !types.ID
- field :iid, !types.ID
- field :title, !types.String
- field :description, types.String
- field :state, types.String
- field :created_at, !Types::TimeType
- field :updated_at, !Types::TimeType
- field :source_project, Types::ProjectType
- field :target_project, !Types::ProjectType
- # Alias for target_project
- field :project, !Types::ProjectType
- field :project_id, !types.Int, property: :target_project_id
- field :source_project_id, types.Int
- field :target_project_id, !types.Int
- field :source_branch, !types.String
- field :target_branch, !types.String
- field :work_in_progress, types.Boolean, property: :work_in_progress?
- field :merge_when_pipeline_succeeds, types.Boolean
- field :sha, types.String, property: :diff_head_sha
- field :merge_commit_sha, types.String
- field :user_notes_count, types.Int
- field :should_remove_source_branch, types.Boolean, property: :should_remove_source_branch?
- field :force_remove_source_branch, types.Boolean, property: :force_remove_source_branch?
- field :merge_status, types.String
- field :in_progress_merge_commit_sha, types.String
- field :merge_error, types.String
- field :allow_maintainer_to_push, types.Boolean
- field :should_be_rebased, types.Boolean, property: :should_be_rebased?
- field :rebase_commit_sha, types.String
- field :rebase_in_progress, types.Boolean, property: :rebase_in_progress?
- field :diff_head_sha, types.String
- field :merge_commit_message, types.String
- field :merge_ongoing, types.Boolean, property: :merge_ongoing?
- field :work_in_progress, types.Boolean, property: :work_in_progress?
- field :source_branch_exists, types.Boolean, property: :source_branch_exists?
- field :mergeable_discussions_state, types.Boolean
- field :web_url, types.String, property: :web_url
- field :upvotes, types.Int
- field :downvotes, types.Int
- field :subscribed, types.Boolean, property: :subscribed?
+ field :id, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::ID_TYPE, null: false
+ field :title, GraphQL::STRING_TYPE, null: false
+ field :description, GraphQL::STRING_TYPE, null: true
+ field :state, GraphQL::STRING_TYPE, null: true
+ field :created_at, Types::TimeType, null: false
+ field :updated_at, Types::TimeType, null: false
+ field :source_project, Types::ProjectType, null: true
+ field :target_project, Types::ProjectType, null: false
+ # Alias for target_project
+ field :project, Types::ProjectType, null: false
+ field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id
+ field :source_project_id, GraphQL::INT_TYPE, null: true
+ field :target_project_id, GraphQL::INT_TYPE, null: false
+ field :source_branch, GraphQL::STRING_TYPE, null: false
+ field :target_branch, GraphQL::STRING_TYPE, null: false
+ field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false
+ field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
+ field :diff_head_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :user_notes_count, GraphQL::INT_TYPE, null: true
+ field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true
+ field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true
+ field :merge_status, GraphQL::STRING_TYPE, null: true
+ field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_error, GraphQL::STRING_TYPE, null: true
+ field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true
+ field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false
+ field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
+ field :diff_head_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_commit_message, GraphQL::STRING_TYPE, null: true
+ field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
+ field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false
+ field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true
+ field :web_url, GraphQL::STRING_TYPE, null: true
+ field :upvotes, GraphQL::INT_TYPE, null: false
+ field :downvotes, GraphQL::INT_TYPE, null: false
+ field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
+ end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index c5061f10239..06ed91c1658 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -1,5 +1,7 @@
-Types::MutationType = GraphQL::ObjectType.define do
- name "Mutation"
+module Types
+ class MutationType < BaseObject
+ graphql_name "Mutation"
- # TODO: Add Mutations as fields
+ # TODO: Add Mutations as fields
+ end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 593c1b67830..9e885d5845a 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -1,63 +1,65 @@
-Types::ProjectType = GraphQL::ObjectType.define do
- name 'Project'
+module Types
+ class ProjectType < BaseObject
+ graphql_name 'Project'
- field :id, !types.ID
+ field :id, GraphQL::ID_TYPE, null: false
- field :full_path, !types.ID
- field :path, !types.String
+ field :full_path, GraphQL::ID_TYPE, null: false
+ field :path, GraphQL::STRING_TYPE, null: false
- field :name_with_namespace, !types.String
- field :name, !types.String
+ field :name_with_namespace, GraphQL::STRING_TYPE, null: false
+ field :name, GraphQL::STRING_TYPE, null: false
- field :description, types.String
+ field :description, GraphQL::STRING_TYPE, null: true
- field :default_branch, types.String
- field :tag_list, types.String
+ field :default_branch, GraphQL::STRING_TYPE, null: true
+ field :tag_list, GraphQL::STRING_TYPE, null: true
- field :ssh_url_to_repo, types.String
- field :http_url_to_repo, types.String
- field :web_url, types.String
+ field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true
+ field :http_url_to_repo, GraphQL::STRING_TYPE, null: true
+ field :web_url, GraphQL::STRING_TYPE, null: true
- field :star_count, !types.Int
- field :forks_count, !types.Int
+ field :star_count, GraphQL::INT_TYPE, null: false
+ field :forks_count, GraphQL::INT_TYPE, null: false
- field :created_at, Types::TimeType
- field :last_activity_at, Types::TimeType
+ field :created_at, Types::TimeType, null: true
+ field :last_activity_at, Types::TimeType, null: true
- field :archived, types.Boolean
+ field :archived, GraphQL::BOOLEAN_TYPE, null: true
- field :visibility, types.String
+ field :visibility, GraphQL::STRING_TYPE, null: true
- field :container_registry_enabled, types.Boolean
- field :shared_runners_enabled, types.Boolean
- field :lfs_enabled, types.Boolean
- field :ff_only_enabled, types.Boolean, property: :merge_requests_ff_only_enabled
+ field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true
- field :avatar_url, types.String do
- resolve ->(project, args, ctx) { project.avatar_url(only_path: false) }
- end
+ field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.avatar_url(only_path: false)
+ end
- %i[issues merge_requests wiki snippets].each do |feature|
- field "#{feature}_enabled", types.Boolean do
- resolve ->(project, args, ctx) { project.feature_available?(feature, ctx[:current_user]) }
+ %i[issues merge_requests wiki snippets].each do |feature|
+ field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.feature_available?(feature, ctx[:current_user])
+ end
end
- end
- field :jobs_enabled, types.Boolean do
- resolve ->(project, args, ctx) { project.feature_available?(:builds, ctx[:current_user]) }
- end
+ field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.feature_available?(:builds, ctx[:current_user])
+ end
- field :public_jobs, types.Boolean, property: :public_builds
+ field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true
- field :open_issues_count, types.Int do
- resolve ->(project, args, ctx) { project.open_issues_count if project.feature_available?(:issues, ctx[:current_user]) }
- end
+ field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
+ end
- field :import_status, types.String
- field :ci_config_path, types.String
+ field :import_status, GraphQL::STRING_TYPE, null: true
+ field :ci_config_path, GraphQL::STRING_TYPE, null: true
- field :only_allow_merge_if_pipeline_succeeds, types.Boolean
- field :request_access_enabled, types.Boolean
- field :only_allow_merge_if_all_discussions_are_resolved, types.Boolean
- field :printing_merge_request_link_enabled, types.Boolean
+ field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
+ field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
+ field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 029bbd098ad..be79c78bf67 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -1,38 +1,21 @@
-Types::QueryType = GraphQL::ObjectType.define do
- name 'Query'
-
- field :project, Types::ProjectType do
- argument :full_path, !types.ID do
- description 'The full path of the project, e.g., "gitlab-org/gitlab-ce"'
- end
-
- authorize :read_project
-
- resolve Loaders::FullPathLoader[:project]
- end
-
- field :merge_request, Types::MergeRequestType do
- argument :project, !types.ID do
- description 'The full path of the target project, e.g., "gitlab-org/gitlab-ce"'
+module Types
+ class QueryType < BaseObject
+ graphql_name 'Query'
+
+ field :project, Types::ProjectType,
+ null: true,
+ resolver: Resolvers::ProjectResolver,
+ description: "Find a project" do
+ authorize :read_project
end
- argument :iid, !types.ID do
- description 'The IID of the merge request, e.g., "1"'
+ field :merge_request, Types::MergeRequestType,
+ null: true,
+ resolver: Resolvers::MergeRequestResolver,
+ description: "Find a merge request" do
+ authorize :read_merge_request
end
- authorize :read_merge_request
-
- resolve Loaders::IidLoader[:merge_request]
- end
-
- # Testing endpoint to validate the API with
- field :echo, types.String do
- argument :text, types.String
-
- resolve -> (obj, args, ctx) do
- username = ctx[:current_user]&.username
-
- "#{username.inspect} says: #{args[:text]}"
- end
+ field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end
end
diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb
index fb717eb3dc7..2333d82ad1e 100644
--- a/app/graphql/types/time_type.rb
+++ b/app/graphql/types/time_type.rb
@@ -1,8 +1,14 @@
-# Taken from http://www.rubydoc.info/github/rmosolgo/graphql-ruby/GraphQL/ScalarType
-Types::TimeType = GraphQL::ScalarType.define do
- name 'Time'
- description 'Time since epoch in fractional seconds'
+module Types
+ class TimeType < BaseScalar
+ graphql_name 'Time'
+ description 'Time represented in ISO 8601'
- coerce_input ->(value, ctx) { Time.at(Float(value)) }
- coerce_result ->(value, ctx) { value.to_f }
+ def self.coerce_input(value, ctx)
+ Time.parse(value)
+ end
+
+ def self.coerce_result(value, ctx)
+ value.iso8601
+ end
+ end
end