sanity-ruby 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,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