operatic 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79842e69c42fad058cef240224911da273e0b801b51762bb4edc062590b1db1c
4
- data.tar.gz: d3262c93d6332c3833b3688425f09d1168bc961fc084b67bcaa148f2700ff60c
3
+ metadata.gz: 56ef955a17eedc8782756d9a79b164662f2d558f16e2e8f48f5991d6452a30df
4
+ data.tar.gz: 660cb2d0ae5118614bcd449a8b3b06b041afe467d762bac8dbe3e10de506b2dc
5
5
  SHA512:
6
- metadata.gz: 212c3d737d571164069bacb305fe196008cbf27d1b81e440c230025715032a84c5df6e1dd2d2d6dc3a9073a5a9ebf010dff712e2f1577b68564f0d0d9594ca33
7
- data.tar.gz: 42d76bcf9a9e5ecef9ebb9e22d5eaa1378ed93bbbc164ac589acf134803b250c2ba8c6b461ef74e6927c6794d78c791d58811316c76ad6bc66fc0f44692326a2
6
+ metadata.gz: 7a432089ae7b3f71c3028bf34e278ea33553e924212bcc83bf5c02b0e3d28923e416e3f1cd054bfa664fce27343dcd76c9beec98dd88b4da2e85a625cb012e37
7
+ data.tar.gz: 271a800712179e79efcb75f1bd267e1dd12900b5eb07837a78aab4c35cffe3914e98733d08436a4cc3d52872934b07c04ae941648b70cd60eb61d0e3b6a6e030
@@ -0,0 +1,7 @@
1
+ version: 2
2
+
3
+ updates:
4
+ - package-ecosystem: github-actions
5
+ directory: "/"
6
+ schedule:
7
+ interval: daily
@@ -8,14 +8,14 @@ jobs:
8
8
  strategy:
9
9
  matrix:
10
10
  ruby:
11
- - '2.5'
12
- - '2.6'
13
11
  - '2.7'
14
12
  - '3.0'
15
13
  - '3.1'
14
+ - '3.2'
15
+ - '3.3'
16
16
  name: Ruby ${{ matrix.ruby }} RSpec
17
17
  steps:
18
- - uses: actions/checkout@v2
18
+ - uses: actions/checkout@v4
19
19
  - uses: ruby/setup-ruby@v1
20
20
  with:
21
21
  bundler-cache: true
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Version 0.7.0 - 2024-05-12
4
+
5
+ - Data within an operation is now gathered on a separate `#data` object that's passed to a concrete `Operatic::Success`/`Operatic::Failure` result instance on completion. Convenience data accessors can be defined on the `Data` object (via the renamed `Operatic.data_attr`) but remain available on the result using the magic of `Result#method_missing`. <https://github.com/benpickles/operatic/pull/18>
6
+ - Require Ruby version 2.7+.
7
+ - Support pattern matching solely against a Result's data. <https://github.com/benpickles/operatic/pull/20>
8
+
9
+ ## Version 0.6.0 - 2022-08-22
10
+
11
+ - Support pattern matching a Result (in Ruby 2.7+). <https://github.com/benpickles/operatic/pull/12>
12
+
3
13
  ## Version 0.5.0 - 2022-06-23
4
14
 
5
15
  - Support custom initialize method to aid compatibility with other libraries. <https://github.com/benpickles/operatic/pull/11>
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in operatic.gemspec
4
4
  gemspec
5
+
6
+ gem 'yard'
data/README.md CHANGED
@@ -24,7 +24,7 @@ class SayHello
24
24
  attr_reader :name
25
25
 
26
26
  # Declare convenience accessors on the result.
27
- result_attr :message
27
+ data_attr :message
28
28
 
29
29
  def call
30
30
  # Exit the method and mark the result as a failure.
@@ -36,12 +36,15 @@ class SayHello
36
36
  end
37
37
 
38
38
  result = SayHello.call(name: 'Dave')
39
+ result.class # => Operatic::Success
40
+ result.failure? # => false
39
41
  result.success? # => true
40
42
  result.message # => "Hello Dave"
41
43
  result[:message] # => "Hello Dave"
42
44
  result.to_h # => {:message=>"Hello Dave"}
43
45
 
44
46
  result = SayHello.call
47
+ result.class # => Operatic::Failure
45
48
  result.failure? # => true
46
49
  result.success? # => false
47
50
  result.message # => nil
@@ -65,10 +68,63 @@ class HellosController < ApplicationController
65
68
  end
66
69
  ```
67
70
 
71
+ ## Pattern matching
72
+
73
+ An Operatic result also supports pattern matching allowing you to match over a tuple of the result class and its data:
74
+
75
+ ```ruby
76
+ case SayHello.call(name: 'Dave')
77
+ in [Operatic::Success, { message: }]
78
+ # Result is a success, do something with the `message` variable.
79
+ in [Operatic::Failure, _]
80
+ # Result is a failure, do something else.
81
+ end
82
+ ```
83
+
84
+ Or match solely against its data:
85
+
86
+ ```ruby
87
+ case SayHello.call(name: 'Dave')
88
+ in message:
89
+ # Result has the `message` key, do something with the variable.
90
+ else
91
+ # Do something else.
92
+ end
93
+ ```
94
+
95
+ Which might be consumed in Rails like this:
96
+
97
+ ```ruby
98
+ class HellosController < ApplicationController
99
+ def create
100
+ case SayHello.call(name: params[:name])
101
+ in [Operatic::Success, { message: }]
102
+ render plain: message
103
+ in [Operatic::Failure, _]
104
+ render :new
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ ## Development
111
+
112
+ Run the tests with:
113
+
114
+ ```
115
+ bundle exec rspec
116
+ ```
117
+
118
+ Generate Yard documentation with:
119
+
120
+ ```
121
+ bundle exec yardoc
122
+ ```
123
+
68
124
  ## License
69
125
 
70
126
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
71
127
 
72
128
  ## Code of Conduct
73
129
 
74
- Everyone interacting in the Operatic project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/benpickles/operatic/blob/master/CODE_OF_CONDUCT.md).
130
+ Everyone interacting in the Operatic project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/benpickles/operatic/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,72 @@
1
+ module Operatic
2
+ class Data
3
+ # Generate a subclass of {Data} with named +attrs+ accessors. This wouldn't
4
+ # normally be called directly, see {ClassMethods#data_attr} for example
5
+ # usage.
6
+ #
7
+ # @param attrs [Array<Symbol>] a list of convenience data accessors.
8
+ def self.define(*attrs)
9
+ Class.new(self) do
10
+ attrs.each do |name|
11
+ define_method name do
12
+ self[name]
13
+ end
14
+
15
+ define_method "#{name}=" do |value|
16
+ self[name] = value
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ # @param kwargs [Hash<Symbol, anything>]
23
+ def initialize(**kwargs)
24
+ @data = kwargs
25
+ end
26
+
27
+ # Return the value for +key+.
28
+ #
29
+ # @param key [Symbol]
30
+ #
31
+ # @return [anything]
32
+ def [](key)
33
+ @data[key]
34
+ end
35
+
36
+ # Set data on the result.
37
+ #
38
+ # @param key [Symbol]
39
+ # @param value [anything]
40
+ def []=(key, value)
41
+ @data[key] = value
42
+ end
43
+
44
+ # @return [self]
45
+ def freeze
46
+ @data.freeze
47
+ super
48
+ end
49
+
50
+ # @param hash [Hash<Symbol, anything>]
51
+ #
52
+ # @return [Data]
53
+ def merge(hash)
54
+ self.class.new.tap { |other|
55
+ other.set_data(@data)
56
+ other.set_data(hash)
57
+ }
58
+ end
59
+
60
+ # @return [Hash<Symbol, anything>]
61
+ def to_h
62
+ @data
63
+ end
64
+
65
+ protected
66
+ def set_data(data)
67
+ data.each do |key, value|
68
+ @data[key] = value
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,91 +1,116 @@
1
1
  module Operatic
2
2
  class Result
3
- # Generate a subclass of {Result} with named +attrs+ accessors. This
4
- # wouldn't normally be called directly, see {ClassMethods#result_attr} for
5
- # example usage.
6
- #
7
- # @param attrs [Array<Symbol>] a list of convenience data accessors.
8
- def self.generate(*attrs)
9
- Class.new(self) do
10
- attrs.each do |name|
11
- define_method name do
12
- @data[name]
13
- end
14
-
15
- define_method "#{name}=" do |value|
16
- @data[name] = value
17
- end
18
- end
19
- end
20
- end
3
+ # @return [Data]
4
+ attr_reader :data
21
5
 
22
- def initialize
23
- @data = {}
24
- @success = true
6
+ # @param data [Data]
7
+ def initialize(data)
8
+ @data = data
25
9
  end
26
10
 
27
- # Read data that's attached to the result.
11
+ # Convenience proxy to read the +key+ from its {#data} object.
12
+ #
13
+ # @param key [Symbol]
14
+ #
15
+ # @return [anything]
28
16
  def [](key)
29
- @data[key]
17
+ data[key]
30
18
  end
31
19
 
32
- # Set data on the result.
33
- def []=(key, value)
34
- @data[key] = value
35
- end
36
-
37
- # Mark the result as a failure, optionally attach +data+ via kwargs, and
38
- # freeze the object so it cannot be modified further.
20
+ # Returns a tuple of self and {#to_h} allowing you to pattern match across
21
+ # both the result's status and its data.
22
+ #
23
+ # @example
24
+ # class SayHello
25
+ # include Operatic
26
+ #
27
+ # def call
28
+ # data[:message] = 'Hello world'
29
+ # end
30
+ # end
39
31
  #
40
- # *Note*: Calling {#success!} or {#failure!} more than once will raise a
41
- # +FrozenError+.
42
- def failure!(**data)
43
- set_data(**data)
44
- @success = false
45
- freeze
32
+ # case SayHello.call
33
+ # in [Operatic::Success, { message: }]
34
+ # # Result is a success, do something with the `message` variable.
35
+ # in [Operatic::Failure, _]
36
+ # # Result is a failure, do something else.
37
+ # end
38
+ #
39
+ # @return [Array(self, Hash<Symbol, anything>)]
40
+ def deconstruct
41
+ [self, to_h]
46
42
  end
47
43
 
48
- def failure?
49
- !@success
44
+ # Pattern match against the result's data via {#to_h}.
45
+ #
46
+ # @example
47
+ # class SayHello
48
+ # include Operatic
49
+ #
50
+ # def call
51
+ # data[:message] = 'Hello world'
52
+ # end
53
+ # end
54
+ #
55
+ # case SayHello.call
56
+ # in message:
57
+ # # Result has the `message` key, do something with the variable.
58
+ # else
59
+ # # Do something else.
60
+ # end
61
+ #
62
+ # @return [Hash<Symbol, anything>]
63
+ def deconstruct_keys(keys = nil)
64
+ to_h
50
65
  end
51
66
 
67
+ # @return [self]
52
68
  def freeze
53
- @data.freeze
69
+ data.freeze
54
70
  super
55
71
  end
56
72
 
57
- # Mark the result as a success, optionally attach +data+ via kwargs, and
58
- # freeze the object so it cannot be modified further.
59
- #
60
- # Calling this is not strictly necessary as a +Result+ defaults to being a
61
- # success, but it's a convenient means of attaching data and of indicating
62
- # intent in the consuming code.
63
- #
64
- # *Note*: Calling {#success!} or {#failure!} more than once will raise a
65
- # +FrozenError+.
66
- def success!(**data)
67
- set_data(**data)
68
- @success = true
69
- freeze
73
+ # Forwards unknown methods to its {#data} object allowing convenience
74
+ # accessors defined via {Data.define} to be available directly on the
75
+ # {Result}.
76
+ def method_missing(name, *args, **kwargs, &block)
77
+ return data.public_send(name, *args, **kwargs, &block) if data.respond_to?(name)
78
+ super
70
79
  end
71
80
 
72
- def success?
73
- @success
81
+ def respond_to?(...)
82
+ super || data.respond_to?(...)
74
83
  end
75
84
 
76
- # Returns the full (frozen) hash of data attached to the result via
77
- # {#success!}, {#failure!}, or convenience accessors added with {.generate}.
85
+ # Convenience proxy to {Data#to_h}.
78
86
  #
79
87
  # @return [Hash<Symbol, anything>]
80
88
  def to_h
81
- @data
89
+ data.to_h
90
+ end
91
+ end
92
+
93
+ class Success < Result
94
+ # @return [false]
95
+ def failure?
96
+ false
97
+ end
98
+
99
+ # @return [true]
100
+ def success?
101
+ true
102
+ end
103
+ end
104
+
105
+ class Failure < Result
106
+ # @return [true]
107
+ def failure?
108
+ true
82
109
  end
83
110
 
84
- private
85
- def set_data(**data)
86
- data.each do |key, value|
87
- @data[key] = value
88
- end
89
- end
111
+ # @return [false]
112
+ def success?
113
+ false
114
+ end
90
115
  end
91
116
  end
@@ -1,3 +1,3 @@
1
1
  module Operatic
2
- VERSION = '0.5.0'
2
+ VERSION = '0.7.0'
3
3
  end
data/lib/operatic.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'operatic/data'
1
2
  require 'operatic/errors'
2
3
  require 'operatic/result'
3
4
  require 'operatic/version'
@@ -9,115 +10,152 @@ module Operatic
9
10
  end
10
11
 
11
12
  module ClassMethods
12
- # The main way of calling an operation.
13
+ # The main way to call an operation. This initializes the class with the
14
+ # supplied +attrs+ keyword arguments and calls {Operatic#call} returning a
15
+ # frozen {Result} instance.
13
16
  #
14
- # The class is instantiated with the supplied +attrs+ keyword arguments and
15
- # calls {Operatic#call} returning a frozen {Result} instance.
17
+ # @param attrs [Hash<Symbol, anything>]
16
18
  #
17
- # @return [Result]
19
+ # @return [Failure, Success]
18
20
  def call(**attrs)
19
- new(**attrs)
20
- .tap(&:call)
21
- .result
22
- .freeze
21
+ operation = new(**attrs)
22
+ operation.call
23
+ operation.result || Success.new(operation.data).freeze
23
24
  end
24
25
 
25
- # Calls {#call} but raises {FailureError} if the returned {Result} is a
26
- # {Result#failure?} - useful for things like background jobs, rake tasks,
27
- # test setups, etc.
26
+ # The same as {#call} but raises {FailureError} if the returned {#result} is
27
+ # a {Failure} - useful for things like background jobs, rake tasks, test
28
+ # setups, etc.
28
29
  #
29
- # @return [Result]
30
+ # @param attrs [Hash<Symbol, anything>]
31
+ #
32
+ # @return [Success]
33
+ #
34
+ # @raise [FailureError] if the operation is not a {Success}
30
35
  def call!(**attrs)
31
36
  call(**attrs).tap { |result|
32
37
  raise FailureError if result.failure?
33
38
  }
34
39
  end
35
40
 
36
- # Define a {Result} subclass with named accessors specific to the class via
37
- # {Result.generate}.
41
+ # Define a class-specific {Data} subclass with the named accessors added via
42
+ # {Data.define}.
38
43
  #
39
44
  # @example
40
45
  # class SayHello
41
46
  # include Operatic
42
47
  #
43
- # attr_reader :name
44
- #
45
- # result_attr :message
48
+ # data_attr :message
46
49
  #
47
50
  # def call
48
- # success!(message: "Hello #{name}")
51
+ # success!(message: "Hello #{@name}")
49
52
  # end
50
53
  # end
51
54
  #
52
55
  # result = SayHello.call(name: 'Dave')
53
- # result.success? # => true
54
- # result.message # => "Hello Dave"
56
+ # result.class # => Operatic::Success
57
+ # result.message # => "Hello Dave"
58
+ # result[:message] # => "Hello Dave"
59
+ # result.to_h # => {:message=>"Hello Dave"}
55
60
  #
56
61
  # @param attrs [Array<Symbol>] a list of convenience data accessors to
57
62
  # define on the {Result}.
58
- def result_attr(*attrs)
59
- @result_class = Result.generate(*attrs)
63
+ def data_attr(*attrs)
64
+ @data_class = Data.define(*attrs)
60
65
  end
61
66
 
62
- def result_class
63
- @result_class || Result
67
+ # @return [Class<Data>]
68
+ def data_class
69
+ @data_class || Data
64
70
  end
65
71
  end
66
72
 
73
+ # @return [Success, Failure]
74
+ attr_reader :result
75
+
76
+ # @param attrs [Hash<Symbol, anything>]
67
77
  def initialize(**attrs)
68
78
  attrs.each do |key, value|
69
79
  instance_variable_set("@#{key}", value)
70
80
  end
71
81
  end
72
82
 
73
- # Override this method with your implementation. Use {#success!} or
74
- # {#failure!} methods to communicate the {#result}'s status and to attach
75
- # data to it. Define convenience result accessors with
76
- # {ClassMethods#result_attr}.
83
+ # Override this method with your implementation. Use {#success!}/{#failure!}
84
+ # to define the status of the result {Success}/{Failure} and attach data.
77
85
  #
78
86
  # @example
79
87
  # class SayHello
80
88
  # include Operatic
81
89
  #
82
- # attr_reader :name
83
- #
84
- # result_attr :message
85
- #
86
90
  # def call
87
- # return failure! unless name
88
- # success!(message: "Hello #{name}")
91
+ # return failure! unless @name
92
+ # success!(message: "Hello #{@name}")
89
93
  # end
90
94
  # end
91
95
  #
92
96
  # result = SayHello.call(name: 'Dave')
93
- # result.failure? # => false
94
- # result.success? # => true
95
- # result.message # => "Hello Dave"
96
- # result.to_h # => {:message=>"Hello Dave"}
97
+ # result.class # => Operatic::Success
98
+ # result.failure? # => false
99
+ # result.success? # => true
100
+ # result[:message] # => "Hello Dave"
101
+ # result.to_h # => {:message=>"Hello Dave"}
97
102
  #
98
103
  # result = SayHello.call
99
- # result.failure? # => true
100
- # result.success? # => false
101
- # result.message # => nil
102
- # result.to_h # => {}
104
+ # result.class # => Operatic::Failure
105
+ # result.failure? # => true
106
+ # result.success? # => false
107
+ # result.to_h # => {}
103
108
  def call
104
109
  end
105
110
 
106
- # Convenience shortcut to the operation's {Result#failure!}.
107
- def failure!(**data)
108
- result.failure!(**data)
111
+ # Any data to be communicated via the operation's result should be added to
112
+ # this {Data} object.
113
+ #
114
+ # *Note*: This will be frozen when returned from an operation.
115
+ #
116
+ # @example
117
+ # class SayHello
118
+ # include Operatic
119
+ #
120
+ # def call
121
+ # data[:message] = "Hello #{@name}"
122
+ # end
123
+ # end
124
+ #
125
+ # result = SayHello.call(name: 'Dave')
126
+ # result.data.to_h # => {:message=>"Dave"}
127
+ # result.data.frozen? # => true
128
+ #
129
+ # @return [Data]
130
+ def data
131
+ @data ||= self.class.data_class.new
109
132
  end
110
133
 
111
- # An instance of {Result} or a subclass generated by
112
- # {ClassMethods#result_attr}.
134
+ # Mark the operation as a failure and prevent further modification to the
135
+ # operation, its result, and its data.
113
136
  #
114
- # @return [Result]
115
- def result
116
- @result ||= self.class.result_class.new
137
+ # @param kwargs [Hash<Symbol, anything>]
138
+ #
139
+ # @raise [FrozenError] if called more than once
140
+ def failure!(**kwargs)
141
+ @result = Failure.new(data.merge(kwargs))
142
+ freeze
143
+ end
144
+
145
+ # @return [self]
146
+ def freeze
147
+ @result.freeze
148
+ super
117
149
  end
118
150
 
119
- # Convenience shortcut to the operation's {Result#success!}.
120
- def success!(**data)
121
- result.success!(**data)
151
+ # Mark the operation as a success and prevent further modification to the
152
+ # operation, its result, and its data.
153
+ #
154
+ # @param kwargs [Hash<Symbol, anything>]
155
+ #
156
+ # @raise [FrozenError] if called more than once
157
+ def success!(**kwargs)
158
+ @result = Success.new(data.merge(kwargs))
159
+ freeze
122
160
  end
123
161
  end
data/operatic.gemspec CHANGED
@@ -14,12 +14,13 @@ Gem::Specification.new do |spec|
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.metadata = {
17
- 'changelog_uri' => 'https://github.com/benpickles/operatic/blob/master/CHANGELOG.md',
17
+ 'changelog_uri' => 'https://github.com/benpickles/operatic/blob/main/CHANGELOG.md',
18
18
  'documentation_uri' => 'https://rubydoc.info/gems/operatic',
19
+ 'rubygems_mfa_required' => 'true',
19
20
  'source_code_uri' => 'https://github.com/benpickles/operatic',
20
21
  }
21
22
 
22
- spec.required_ruby_version = '>= 2.5.0'
23
+ spec.required_ruby_version = '>= 2.7.0'
23
24
 
24
25
  # Specify which files should be added to the gem when it is released.
25
26
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: operatic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Pickles
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-23 00:00:00.000000000 Z
11
+ date: 2024-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -59,6 +59,7 @@ executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
+ - ".github/dependabot.yml"
62
63
  - ".github/workflows/ruby.yml"
63
64
  - ".gitignore"
64
65
  - ".rspec"
@@ -71,6 +72,7 @@ files:
71
72
  - bin/console
72
73
  - bin/setup
73
74
  - lib/operatic.rb
75
+ - lib/operatic/data.rb
74
76
  - lib/operatic/errors.rb
75
77
  - lib/operatic/result.rb
76
78
  - lib/operatic/version.rb
@@ -79,8 +81,9 @@ homepage: https://github.com/benpickles/operatic
79
81
  licenses:
80
82
  - MIT
81
83
  metadata:
82
- changelog_uri: https://github.com/benpickles/operatic/blob/master/CHANGELOG.md
84
+ changelog_uri: https://github.com/benpickles/operatic/blob/main/CHANGELOG.md
83
85
  documentation_uri: https://rubydoc.info/gems/operatic
86
+ rubygems_mfa_required: 'true'
84
87
  source_code_uri: https://github.com/benpickles/operatic
85
88
  post_install_message:
86
89
  rdoc_options: []
@@ -90,14 +93,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
93
  requirements:
91
94
  - - ">="
92
95
  - !ruby/object:Gem::Version
93
- version: 2.5.0
96
+ version: 2.7.0
94
97
  required_rubygems_version: !ruby/object:Gem::Requirement
95
98
  requirements:
96
99
  - - ">="
97
100
  - !ruby/object:Gem::Version
98
101
  version: '0'
99
102
  requirements: []
100
- rubygems_version: 3.3.7
103
+ rubygems_version: 3.5.9
101
104
  signing_key:
102
105
  specification_version: 4
103
106
  summary: A minimal standard interface for your operations