re_sorcery 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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ module Fielded
5
+ module ExpandInternalFields
6
+ extend Helpers
7
+
8
+ # Used internally to expand deeply nested `Fielded` structures
9
+ #
10
+ # `Hash` is intentionally *not* expanded. Create a `Fielded` class instead.
11
+ #
12
+ # Similarly, `nil` is intentionally rejected here. Use a type that more
13
+ # meaningfully represents an empty value instead.
14
+ def self.expand(obj)
15
+ case obj
16
+ when ReSorcery
17
+ obj.resource
18
+ when Fielded
19
+ obj.fields
20
+ when Linked
21
+ obj.links
22
+ when String, Numeric, Symbol, TrueClass, FalseClass
23
+ ok(obj)
24
+ when Array
25
+ expand_for_array(obj)
26
+ when URI
27
+ ok(obj.to_s)
28
+ when Hash
29
+ err("`Hash` cannot be safely expanded as a `field`. Use a `Fielded` class instead.")
30
+ when NilClass
31
+ err("`nil` cannot be returned as a `field`")
32
+ else
33
+ err("Cannot deeply expand fields of class #{obj.class}")
34
+ end
35
+ end
36
+
37
+ def self.expand_for_array(array)
38
+ array.each_with_index.inject(ok([])) do |result_array, (element, index)|
39
+ result_array.and_then do |ok_array|
40
+ expand(element)
41
+ .map { |good| ok_array << good }
42
+ .map_error { |error| "Error at index `#{index}` of Array: #{error}" }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ private_constant :ExpandInternalFields
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 're_sorcery/fielded/expand_internal_fields'
4
+
5
+ module ReSorcery
6
+ module Fielded
7
+ include Helpers
8
+
9
+ module ClassMethods
10
+ include Decoder::BuiltinDecoders
11
+
12
+ private
13
+
14
+ # Set a field for instances of a class
15
+ #
16
+ # There is intentionally no way to make fields optionally nil. Use a type
17
+ # that more meaningfully represents an empty value instead, such as a
18
+ # `Maybe` type or discriminated unions.
19
+ #
20
+ # @param [Symbol] name
21
+ # @param [arg of Decoder.is] type @see `ReSorcery::Decoder.is` for details
22
+ # @param [Proc] pro: in the context of an instance of the class, return the value of the field
23
+ def field(name, type, pro = -> { send(name) })
24
+ ArgCheck['name', name, Symbol]
25
+ ArgCheck['pro', pro, Proc]
26
+
27
+ (@fields ||= {})[name] = { type: is(type), pro: pro }
28
+ end
29
+ end
30
+
31
+ def self.included(base)
32
+ base.extend(ClassMethods)
33
+ end
34
+
35
+ # Returns the `Decoder#test`ed fields of the object, wrapped in a `Result`
36
+ #
37
+ # If all the `Decoder`s pass, this will return an `Ok`. If any of them
38
+ # fail, it will return an `Err` instead.
39
+ #
40
+ # @return [Result<String, Hash>]
41
+ def fields
42
+ self.class.instance_exec { @fields ||= [] }.inject(ok({})) do |result_hash, (name, field_hash)|
43
+ result_hash.and_then do |ok_hash|
44
+ field_hash[:type].test(instance_exec(&field_hash[:pro]))
45
+ .and_then { |tested| ExpandInternalFields.expand(tested) }
46
+ .map { |fielded| ok_hash.merge(name => fielded) }
47
+ .map_error { |error| "Error at field `#{name}` of `#{self.class}`: #{error}" }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ module ReSorcery
2
+ module Helpers
3
+ private
4
+
5
+ def just(value)
6
+ Maybe::Just.new(value)
7
+ end
8
+
9
+ def nothing
10
+ Maybe::Nothing.new
11
+ end
12
+
13
+ # Wrap a possibly-nil value in a `Maybe`
14
+ #
15
+ # @param value The value to wrap in a `Maybe`.
16
+ # @return [Maybe]
17
+ def nillable(value)
18
+ value.nil? ? nothing : just(value)
19
+ end
20
+
21
+ def ok(value)
22
+ Result::Ok.new(value)
23
+ end
24
+
25
+ def err(e)
26
+ Result::Err.new(e)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ module Linked
5
+ class LinkClassFactory
6
+ extend Decoder::BuiltinDecoders
7
+
8
+ def self.valid_rels
9
+ ReSorcery.configuration.fetch(
10
+ :link_rels,
11
+ %w[
12
+ self
13
+ create
14
+ update
15
+ destroy
16
+ ],
17
+ )
18
+ end
19
+
20
+ def self.valid_methods
21
+ ReSorcery.configuration.fetch(
22
+ :link_methods,
23
+ %w[
24
+ get
25
+ post
26
+ patch
27
+ put
28
+ delete
29
+ ],
30
+ )
31
+ end
32
+
33
+ URI_ABLE = is(String, URI).and do |s|
34
+ next true if s.is_a?(URI)
35
+
36
+ begin
37
+ ok(URI.parse(s))
38
+ rescue URI::InvalidURIError
39
+ err("Not a valid URI: #{s}")
40
+ end
41
+ end
42
+
43
+ def self.make_link_class
44
+ default_method = valid_methods.first
45
+ this = self
46
+
47
+ Class.new do
48
+ include Fielded
49
+
50
+ def initialize(args)
51
+ @args = args
52
+ end
53
+
54
+ field :rel, is(*this.valid_rels), -> { @args[:rel] }
55
+ field :href, URI_ABLE, -> { @args[:href] }
56
+ field :method, is(*this.valid_methods), -> { @args.fetch(:method, default_method) }
57
+ field :type, String, -> { @args.fetch(:type, 'application/json') }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 're_sorcery/linked/link_class_factory'
4
+
5
+ module ReSorcery
6
+ module Linked
7
+ include Helpers
8
+
9
+ module ClassMethods
10
+ private
11
+
12
+ # Define a set of `Link`s for a class
13
+ #
14
+ # The block is evaluated in the context of an instance of the class, so
15
+ # the set of `Link`s can be contextualized. For example, if the current
16
+ # user doesn't have permissions to edit the object, the "update" `Link`
17
+ # can be left out:
18
+ #
19
+ # class MyObject
20
+ # include Linked
21
+ # attr_reader :id, :current_user
22
+ #
23
+ # def initialize(id, current_user)
24
+ # @id = id
25
+ # @current_user = current_user
26
+ # end
27
+ #
28
+ # links do
29
+ # link 'self', "/my_objects/#{id}"
30
+ # link 'update', "/my_objects/#{id}", 'put' if current_user.can_update?(self)
31
+ # link 'destroy', "/my_objects/#{id}", 'delete' if current_user.can_destroy?(self)
32
+ # end
33
+ # end
34
+ #
35
+ # The result of calling the block is not cached.
36
+ def links(&block)
37
+ @links_proc = block
38
+ end
39
+ end
40
+
41
+ def self.included(base)
42
+ base.extend(ClassMethods)
43
+ end
44
+
45
+ def self.link_class
46
+ @link_class ||= LinkClassFactory.make_link_class
47
+ end
48
+
49
+ def links
50
+ instance_exec(&self.class.instance_exec { @links_proc ||= -> {} })
51
+ created_links = (@_created_links ||= [])
52
+ @_created_links = [] # Clear out so `links` can run cleanly next time
53
+
54
+ created_links.each_with_index.inject(ok([])) do |result_array, (link_result, index)|
55
+ result_array.and_then do |ok_array|
56
+ link_result
57
+ .map { |link| ok_array << link }
58
+ .map_error { |error| "Error with Link at index #{index}: #{error}" }
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Define a `Link` for an object
66
+ #
67
+ # @see `ReSorcery::Linked::Link#initialize` for param types
68
+ def link(rel, href, method = 'get', type = 'application/json')
69
+ klass = Linked.link_class
70
+ (@_created_links ||= []) << klass.new(rel: rel, href: href, method: method, type: type).fields
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ module Maybe
5
+ class Just
6
+ include Fielded
7
+
8
+ field :kind, :just, -> { :just }
9
+ field :value, Decoder.new { true }, -> { @value }
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def and_then(&block)
16
+ ArgCheck['block', block.call(@value), Just, Nothing]
17
+ end
18
+
19
+ def map(&block)
20
+ Just.new(block.call(@value))
21
+ end
22
+
23
+ def or_else
24
+ self
25
+ end
26
+
27
+ def get_or_else
28
+ @value
29
+ end
30
+
31
+ def assign(name, &block)
32
+ raise Error::NonHashAssignError, @value unless @value.is_a?(Hash)
33
+
34
+ ArgCheck['block', block.call(@value), Just, Nothing]
35
+ .map { |k| @value.merge(name => k) }
36
+ end
37
+
38
+ def ==(other)
39
+ other.class == Just && other.instance_eval { @value } == @value
40
+ end
41
+
42
+ def as_json(*)
43
+ {
44
+ kind: :just,
45
+ value: @value,
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ module Maybe
5
+ class Nothing
6
+ include Fielded
7
+
8
+ field :kind, :nothing, -> { :nothing }
9
+
10
+ def and_then
11
+ self
12
+ end
13
+
14
+ def map
15
+ self
16
+ end
17
+
18
+ def or_else(&block)
19
+ ArgCheck['block', block.call, Just, Nothing]
20
+ end
21
+
22
+ def get_or_else(&block)
23
+ block.call
24
+ end
25
+
26
+ def assign(_name)
27
+ self
28
+ end
29
+
30
+ def ==(other)
31
+ other.class == Nothing
32
+ end
33
+
34
+ def as_json(*)
35
+ {
36
+ kind: :nothing,
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ module Maybe
5
+ end
6
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ module Result
5
+ class Err
6
+ def initialize(err)
7
+ @err = err
8
+ end
9
+
10
+ def and_then
11
+ self
12
+ end
13
+
14
+ def map
15
+ self
16
+ end
17
+
18
+ def map_error(&block)
19
+ Err.new(block.call(@err))
20
+ end
21
+
22
+ def or_else(&block)
23
+ ArgCheck['block', block.call(@err), Ok, Err]
24
+ end
25
+
26
+ def assign(_name)
27
+ self
28
+ end
29
+
30
+ def cata(ok:, err:)
31
+ err.call(@err)
32
+ end
33
+
34
+ def ==(other)
35
+ other.class == Err && other.instance_eval { @err } == @err
36
+ end
37
+
38
+ def as_json(*)
39
+ {
40
+ kind: :err,
41
+ value: @err,
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ module Result
5
+ class Ok
6
+ def initialize(value)
7
+ @value = value
8
+ end
9
+
10
+ def and_then(&block)
11
+ ArgCheck['block', block.call(@value), Ok, Err]
12
+ end
13
+
14
+ def map(&block)
15
+ Ok.new(block.call(@value))
16
+ end
17
+
18
+ def map_error
19
+ self
20
+ end
21
+
22
+ def or_else
23
+ self
24
+ end
25
+
26
+ def assign(name, &block)
27
+ raise Error::NonHashAssignError, @value unless @value.is_a?(Hash)
28
+
29
+ ArgCheck['block', block.call(@value), Ok, Err]
30
+ .map { |k| @value.merge(name => k) }
31
+ end
32
+
33
+ def cata(ok:, err:)
34
+ ok.call(@value)
35
+ end
36
+
37
+ def ==(other)
38
+ other.class == Ok && other.instance_eval { @value } == @value
39
+ end
40
+
41
+ def as_json(*)
42
+ {
43
+ kind: :ok,
44
+ value: @value,
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 're_sorcery/result/ok'
4
+ require 're_sorcery/result/err'
5
+
6
+ module ReSorcery
7
+ module Result
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReSorcery
4
+ VERSION = "0.1.0"
5
+ end
data/lib/re_sorcery.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 're_sorcery/version'
4
+ require 're_sorcery/error'
5
+ require 're_sorcery/arg_check'
6
+ require 're_sorcery/maybe'
7
+ require 're_sorcery/result'
8
+ require 're_sorcery/helpers'
9
+ require 're_sorcery/decoder'
10
+ require 're_sorcery/fielded'
11
+ require 're_sorcery/maybe/just'
12
+ require 're_sorcery/maybe/nothing'
13
+ require 're_sorcery/linked'
14
+ require 're_sorcery/configuration'
15
+
16
+ module ReSorcery
17
+ include Fielded
18
+ include Linked
19
+ include Helpers
20
+ extend Configuration
21
+
22
+ def self.included(base)
23
+ base.extend Fielded::ClassMethods
24
+ base.extend Linked::ClassMethods
25
+ @configured = "included at #{caller_locations.first}"
26
+ end
27
+
28
+ def resource
29
+ Result::Ok.new({})
30
+ .assign(:payload) { fields }
31
+ .assign(:links) { links }
32
+ end
33
+
34
+ def as_json(*)
35
+ resource.cata(
36
+ ok: ->(r) { r },
37
+ err: ->(e) { raise Error::InvalidResourceError, e },
38
+ )
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "re_sorcery/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "re_sorcery"
7
+ spec.version = ReSorcery::VERSION
8
+ spec.authors = ["Spencer Christiansen"]
9
+ spec.email = ["jc.spencer92@gmail.com"]
10
+
11
+ spec.summary = "Create resources with run-time payload type checking and link validation"
12
+ spec.homepage = "https://github.com/spejamchr/re_sorcery"
13
+ spec.license = "MIT"
14
+
15
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
19
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 2.0"
31
+ spec.add_development_dependency "minitest", "~> 5.0"
32
+ spec.add_development_dependency "pry"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: re_sorcery
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Spencer Christiansen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ description:
70
+ email:
71
+ - jc.spencer92@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - Gemfile.lock
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/setup
85
+ - lib/re_sorcery.rb
86
+ - lib/re_sorcery/arg_check.rb
87
+ - lib/re_sorcery/configuration.rb
88
+ - lib/re_sorcery/decoder.rb
89
+ - lib/re_sorcery/decoder/builtin_decoders.rb
90
+ - lib/re_sorcery/error.rb
91
+ - lib/re_sorcery/fielded.rb
92
+ - lib/re_sorcery/fielded/expand_internal_fields.rb
93
+ - lib/re_sorcery/helpers.rb
94
+ - lib/re_sorcery/linked.rb
95
+ - lib/re_sorcery/linked/link_class_factory.rb
96
+ - lib/re_sorcery/maybe.rb
97
+ - lib/re_sorcery/maybe/just.rb
98
+ - lib/re_sorcery/maybe/nothing.rb
99
+ - lib/re_sorcery/result.rb
100
+ - lib/re_sorcery/result/err.rb
101
+ - lib/re_sorcery/result/ok.rb
102
+ - lib/re_sorcery/version.rb
103
+ - re_sorcery.gemspec
104
+ homepage: https://github.com/spejamchr/re_sorcery
105
+ licenses:
106
+ - MIT
107
+ metadata:
108
+ homepage_uri: https://github.com/spejamchr/re_sorcery
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.0.3.1
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Create resources with run-time payload type checking and link validation
128
+ test_files: []