sanity-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Http
5
+ class Patch
6
+ include Sanity::Http::Mutation
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ using Sanity::Refinements::Strings
6
+
7
+ module Sanity
8
+ module Http
9
+ module Query
10
+ class << self
11
+ def included(base)
12
+ base.extend(ClassMethods)
13
+ base.extend(Forwardable)
14
+ base.delegate(%i[project_id api_version dataset token] => :"Sanity.config")
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def call(**args)
20
+ new(**args).call
21
+ end
22
+ end
23
+
24
+ attr_reader :resource_klass, :result_wrapper
25
+
26
+ # @todo Add query support
27
+ def initialize(**args)
28
+ @resource_klass = args.delete(:resource_klass)
29
+ @result_wrapper = args.delete(:result_wrapper) || Sanity::Http::Results
30
+ end
31
+
32
+ # @todo Add query support
33
+ def call
34
+ http = Net::HTTP.new(uri.host, uri.port)
35
+
36
+ http.use_ssl = uri.scheme == "https"
37
+
38
+ request = Module.const_get("Net::HTTP::#{method.to_s.classify}").new(uri, headers)
39
+
40
+ request.body = request_body
41
+
42
+ http.request(request).then do |result|
43
+ data = JSON.parse(result.body)
44
+
45
+ block_given? ? yield(result_wrapper.call(data)) : result_wrapper.call(data)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def request_body
52
+ end
53
+
54
+ def method
55
+ :get
56
+ end
57
+
58
+ def base_url
59
+ "https://#{project_id}.api.sanity.io/#{api_version}/#{api_endpoint}/#{dataset}"
60
+ end
61
+
62
+ def headers
63
+ {
64
+ "Content-Type": "application/json",
65
+ Authorization: "Bearer #{token}"
66
+ }
67
+ end
68
+
69
+ def uri
70
+ URI(base_url)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Http
5
+ class Results
6
+ class << self
7
+ def call(result)
8
+ new(result).call
9
+ end
10
+ end
11
+
12
+ attr_reader :raw_result
13
+
14
+ def initialize(result)
15
+ @raw_result = result
16
+ end
17
+
18
+ # TODO: parse the JSON and return what the user asked for
19
+ # whether that just be the response, the document ids, or the
20
+ # the document object(s) the user mutated
21
+ def call
22
+ raw_result
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hash#except! was added in Ruby 3
4
+ using Sanity::Refinements::Hashes if RUBY_VERSION.to_i < 3.0
5
+
6
+ module Sanity
7
+ module Http
8
+ class Where
9
+ include Sanity::Http::Query
10
+ delegate where_api_endpoint: :resource_klass
11
+ alias_method :api_endpoint, :where_api_endpoint
12
+
13
+ attr_reader :groq, :use_post, :groq_attributes, :variables
14
+
15
+ def initialize(**args)
16
+ super
17
+ @groq = args.delete(:groq) || ""
18
+ @variables = args.delete(:variables) || {}
19
+ @use_post = args.delete(:use_post) || false
20
+
21
+ @groq_attributes = args.except(:groq, :use_post, :resource_klass, :result_wrapper)
22
+ end
23
+
24
+ private
25
+
26
+ def method
27
+ use_post ? :post : :get
28
+ end
29
+
30
+ def uri
31
+ super.tap do |obj|
32
+ obj.query = URI.encode_www_form(query_and_variables) unless use_post
33
+ end
34
+ end
35
+
36
+ def query_and_variables
37
+ if use_post
38
+ {params: variables}
39
+ else
40
+ {}.tap do |hash|
41
+ variables.each do |key, value|
42
+ hash["$#{key}"] = "\"#{value}\""
43
+ end
44
+ end
45
+ end.merge(query: groq_query)
46
+ end
47
+
48
+ def groq_query
49
+ groq.empty? ? Sanity::Groqify.call(**groq_attributes) : groq
50
+ end
51
+
52
+ def request_body
53
+ return unless use_post
54
+
55
+ query_and_variables.to_json
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ # Mutatable is responsible for setting the appropriate class methods
5
+ # that invoke Sanity::Http's mutatable classes
6
+ #
7
+ # The mutatable marco can limit what queries are accessible to the
8
+ # mutatable object
9
+ #
10
+ # @example provides default class methods
11
+ # mutatable
12
+ #
13
+ # @example only add the `.create` method
14
+ # mutatable only: %i(create)
15
+ #
16
+ # @example only add the `.create_or_replace`& `#create_or_replace` methods
17
+ # mutatable only: %i(create_or_replace)
18
+ #
19
+ using Sanity::Refinements::Strings
20
+
21
+ module Mutatable
22
+ class << self
23
+ def included(base)
24
+ base.extend(ClassMethods)
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+ DEFAULT_KLASS_MUTATIONS = %i[create create_or_replace create_if_not_exists patch delete].freeze
30
+ DEFAULT_INSTANCE_MUTATIONS = %i[create create_or_replace create_if_not_exists delete].freeze
31
+ ALL_MUTATIONS = DEFAULT_KLASS_MUTATIONS | DEFAULT_INSTANCE_MUTATIONS
32
+
33
+ private
34
+
35
+ def mutatable(**options)
36
+ options.fetch(:only, ALL_MUTATIONS).each do |mutation|
37
+ if DEFAULT_KLASS_MUTATIONS.include? mutation.to_sym
38
+ define_singleton_method(mutation) do |**args|
39
+ Module.const_get("Sanity::Http::#{mutation.to_s.classify}").call(**args.merge(resource_klass: self))
40
+ end
41
+ end
42
+
43
+ if DEFAULT_INSTANCE_MUTATIONS.include? mutation.to_sym
44
+ define_method(mutation) do |**args|
45
+ Module.const_get("Sanity::Http::#{mutation.to_s.classify}").call(
46
+ **args.merge(params: attributes, resource_klass: self.class)
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ define_singleton_method("mutatable_api_endpoint") { options.fetch(:api_endpoint, "") }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ # Queryable is responsible for setting the appropriate class methods
5
+ # that invoke Sanity::Http's query classes
6
+ #
7
+ # The queryable marco can limit what queries are accessible to the
8
+ # queryable object
9
+ #
10
+ # @example provides default class methods
11
+ # queryable
12
+ #
13
+ # @example only add the `.where` method
14
+ # queryable only: %i(where)
15
+ #
16
+ # @example only add the `.find` method
17
+ # queryable only: %i(find)
18
+ #
19
+ using Sanity::Refinements::Strings
20
+
21
+ module Queryable
22
+ class << self
23
+ def included(base)
24
+ base.extend(ClassMethods)
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+ DEFAULT_KLASS_QUERIES = %i[find where].freeze
30
+
31
+ # See https://www.sanity.io/docs/http-query & https://www.sanity.io/docs/http-doc
32
+ QUERY_ENDPOINTS = {
33
+ find: "data/doc",
34
+ where: "data/query"
35
+ }.freeze
36
+
37
+ private
38
+
39
+ # @private
40
+ def queryable(**options)
41
+ options.fetch(:only, DEFAULT_KLASS_QUERIES).each do |query|
42
+ define_singleton_method(query) do |**args|
43
+ Module.const_get("Sanity::Http::#{query.to_s.classify}").call(**args.merge(resource_klass: self))
44
+ end
45
+ define_singleton_method("#{query}_api_endpoint") { QUERY_ENDPOINTS[query] }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sanity/refinements/strings"
4
+ require "sanity/refinements/arrays"
5
+ require "sanity/refinements/hashes"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Refinements
5
+ # Array refinements based on ActiveSupport methods.
6
+ #
7
+ # Using refinements as to:
8
+ # 1) not pollute the global namespace
9
+ # 2) not conflict with ActiveSupport in a Rails based project
10
+ #
11
+ # These methods are defined in the way as needed in this gem. They
12
+ # are not meant to replace the more robust ActiveSupport methods.
13
+ #
14
+ # Defining these here, removes the need for adding ActiveSupport
15
+ # as a dependency
16
+ module Arrays
17
+ refine Array.singleton_class do
18
+ def wrap(object)
19
+ if object.nil?
20
+ []
21
+ elsif object.respond_to?(:to_ary)
22
+ object.to_ary || [object]
23
+ else
24
+ [object]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Refinements
5
+ module Hashes
6
+ refine Hash do
7
+ # Defined in Ruby >= 3
8
+ def except!(*keys)
9
+ keys.each { |key| delete(key) }
10
+ self
11
+ end
12
+
13
+ # Defined in Ruby >= 3
14
+ def except(*keys)
15
+ dup.except!(*keys)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Refinements
5
+ # String refinements based on ActiveSupport methods.
6
+ #
7
+ # Using refinements as to:
8
+ # 1) not pollute the global namespace
9
+ # 2) not conflict with ActiveSupport in a Rails based project
10
+ #
11
+ # These methods are defined in the way as needed in this gem. They
12
+ # are not meant to replace the more robust ActiveSupport methods.
13
+ #
14
+ # Defining these here, removes the need for adding ActiveSupport
15
+ # as a dependency
16
+ module Strings
17
+ refine String do
18
+ def camelize_lower
19
+ split("_")[0..].each_with_index.map do |val, idx|
20
+ idx != 0 ? val.capitalize : val
21
+ end.join
22
+ end
23
+
24
+ def classify
25
+ split("_").map(&:capitalize).join
26
+ end
27
+
28
+ def demodulize
29
+ split("::")[-1]
30
+ end
31
+
32
+ def underscore
33
+ split(/(?=[A-Z])/).map(&:downcase).join("_")
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ # Sanity::Resource is the base class used by
5
+ # the sanity resources defined within this gem.
6
+ #
7
+ # Out of the box it includes the following mixins:
8
+ # Sanity::Attributable
9
+ # Sanity::Mutatable
10
+ # Sanity::Queryable
11
+ #
12
+ # Sanity::Document and Sanity::Asset both inherit
13
+ # from Sanity::Resource
14
+ #
15
+ # Any PORO in your project could become a
16
+ # Sanity::Resource via inheritance
17
+ #
18
+ # @example inherit from Sanity::Resource
19
+ # class User < Sanity::Resource
20
+ # attribute :first_name, default: ""
21
+ # queryable
22
+ # mutatable
23
+ # end
24
+ #
25
+ class Resource
26
+ include Sanity::Attributable
27
+ include Sanity::Mutatable
28
+ include Sanity::Queryable
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sanity/resources/document"
4
+ require "sanity/resources/asset"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ class Asset < Sanity::Resource
5
+ mutatable only: %i[create], api_endpoint: "asset/images"
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ # Sanity::Document is the core resource for interacting
5
+ # with Sanity's HTTP API. This class provides out of
6
+ # the box query and mutation methods for interacting
7
+ # with the API.
8
+ #
9
+ # @example create a new document object in memory
10
+ # Sanity::Document.new(_id: 1, _type: "post")
11
+ #
12
+ # @example invoke the api operations to create a document
13
+ # Sanity::Document.create(params: {_type: "post", title: "A new blog post"})
14
+ #
15
+ # @example invoke the api operations to delete a document
16
+ # Sanity::Document.delete(params: {id: "1234"})
17
+ #
18
+ class Document < Sanity::Resource
19
+ attribute :_id, default: ""
20
+ attribute :_type, default: ""
21
+ # See https://www.sanity.io/docs/http-mutations#ac77879076d4
22
+ mutatable api_endpoint: "data/mutate"
23
+ queryable
24
+ end
25
+ end