facera 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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/gem-push.yml +42 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +39 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +137 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +309 -0
- data/Rakefile +6 -0
- data/examples/01_core_dsl.rb +132 -0
- data/examples/02_facet_system.rb +216 -0
- data/examples/03_api_generation.rb +117 -0
- data/examples/04_auto_mounting.rb +182 -0
- data/examples/05_adapters.rb +196 -0
- data/examples/README.md +184 -0
- data/examples/server/README.md +376 -0
- data/examples/server/adapters/payment_adapter.rb +139 -0
- data/examples/server/application.rb +17 -0
- data/examples/server/config/facera.rb +33 -0
- data/examples/server/config.ru +10 -0
- data/examples/server/cores/payment_core.rb +82 -0
- data/examples/server/facets/external_facet.rb +38 -0
- data/examples/server/facets/internal_facet.rb +33 -0
- data/examples/server/facets/operator_facet.rb +48 -0
- data/facera.gemspec +30 -0
- data/img/facera.png +0 -0
- data/lib/facera/adapter.rb +83 -0
- data/lib/facera/attribute.rb +95 -0
- data/lib/facera/auto_mount.rb +124 -0
- data/lib/facera/capability.rb +117 -0
- data/lib/facera/capability_access.rb +59 -0
- data/lib/facera/configuration.rb +83 -0
- data/lib/facera/context.rb +29 -0
- data/lib/facera/core.rb +65 -0
- data/lib/facera/dsl.rb +41 -0
- data/lib/facera/entity.rb +50 -0
- data/lib/facera/error_formatter.rb +100 -0
- data/lib/facera/errors.rb +40 -0
- data/lib/facera/executor.rb +265 -0
- data/lib/facera/facet.rb +103 -0
- data/lib/facera/field_visibility.rb +69 -0
- data/lib/facera/generators/core_generator.rb +23 -0
- data/lib/facera/generators/facet_generator.rb +25 -0
- data/lib/facera/generators/install_generator.rb +64 -0
- data/lib/facera/generators/templates/core.rb.tt +49 -0
- data/lib/facera/generators/templates/facet.rb.tt +23 -0
- data/lib/facera/grape/api_generator.rb +59 -0
- data/lib/facera/grape/endpoint_generator.rb +316 -0
- data/lib/facera/grape/entity_generator.rb +89 -0
- data/lib/facera/grape.rb +14 -0
- data/lib/facera/introspection.rb +111 -0
- data/lib/facera/introspection_api.rb +66 -0
- data/lib/facera/invariant.rb +26 -0
- data/lib/facera/loader.rb +153 -0
- data/lib/facera/openapi_generator.rb +338 -0
- data/lib/facera/railtie.rb +51 -0
- data/lib/facera/registry.rb +34 -0
- data/lib/facera/tasks/routes.rake +66 -0
- data/lib/facera/version.rb +3 -0
- data/lib/facera.rb +35 -0
- metadata +137 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
class FieldVisibility
|
|
3
|
+
attr_reader :entity_name, :visible_fields, :hidden_fields, :field_aliases, :computed_fields
|
|
4
|
+
|
|
5
|
+
def initialize(entity_name)
|
|
6
|
+
@entity_name = entity_name.to_sym
|
|
7
|
+
@visible_fields = nil # nil means all fields visible
|
|
8
|
+
@hidden_fields = []
|
|
9
|
+
@field_aliases = {}
|
|
10
|
+
@computed_fields = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fields(*field_names)
|
|
14
|
+
if field_names.first == :all
|
|
15
|
+
@visible_fields = :all
|
|
16
|
+
else
|
|
17
|
+
@visible_fields = field_names.map(&:to_sym)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def hide(*field_names)
|
|
22
|
+
@hidden_fields = field_names.map(&:to_sym)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def alias_field(source, as:)
|
|
26
|
+
@field_aliases[source.to_sym] = as.to_sym
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def computed(field_name, &block)
|
|
30
|
+
raise Error, "Computed field '#{field_name}' must have a block" unless block_given?
|
|
31
|
+
@computed_fields[field_name.to_sym] = block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visible?(field_name)
|
|
35
|
+
field_sym = field_name.to_sym
|
|
36
|
+
|
|
37
|
+
# If explicitly hidden, not visible
|
|
38
|
+
return false if @hidden_fields.include?(field_sym)
|
|
39
|
+
|
|
40
|
+
# If visible_fields is nil or :all, visible (unless hidden)
|
|
41
|
+
return true if @visible_fields.nil? || @visible_fields == :all
|
|
42
|
+
|
|
43
|
+
# Otherwise check if in visible list
|
|
44
|
+
@visible_fields.include?(field_sym)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def aliased_name(field_name)
|
|
48
|
+
@field_aliases[field_name.to_sym] || field_name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def visible_field_names(entity)
|
|
52
|
+
base_fields = if @visible_fields == :all || @visible_fields.nil?
|
|
53
|
+
entity.attributes.keys
|
|
54
|
+
else
|
|
55
|
+
@visible_fields
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Filter out hidden fields
|
|
59
|
+
base_fields.reject { |f| @hidden_fields.include?(f) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def all_visible_fields(entity)
|
|
63
|
+
base_fields = visible_field_names(entity)
|
|
64
|
+
computed_field_names = @computed_fields.keys
|
|
65
|
+
|
|
66
|
+
base_fields + computed_field_names
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
if defined?(Rails)
|
|
2
|
+
require 'rails/generators/base'
|
|
3
|
+
|
|
4
|
+
module Facera
|
|
5
|
+
module Generators
|
|
6
|
+
class CoreGenerator < Rails::Generators::NamedBase
|
|
7
|
+
source_root File.expand_path('../templates', __FILE__)
|
|
8
|
+
|
|
9
|
+
desc "Generate a new Facera core"
|
|
10
|
+
|
|
11
|
+
def create_core_file
|
|
12
|
+
template 'core.rb.tt', File.join('app/cores', "#{file_name}_core.rb")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def entity_name
|
|
18
|
+
file_name.singularize
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
if defined?(Rails)
|
|
2
|
+
require 'rails/generators/base'
|
|
3
|
+
|
|
4
|
+
module Facera
|
|
5
|
+
module Generators
|
|
6
|
+
class FacetGenerator < Rails::Generators::NamedBase
|
|
7
|
+
source_root File.expand_path('../templates', __FILE__)
|
|
8
|
+
|
|
9
|
+
desc "Generate a new Facera facet"
|
|
10
|
+
|
|
11
|
+
class_option :core, type: :string, required: true, desc: "The core this facet belongs to"
|
|
12
|
+
|
|
13
|
+
def create_facet_file
|
|
14
|
+
template 'facet.rb.tt', File.join('app/facets', "#{file_name}_facet.rb")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def core_name
|
|
20
|
+
options[:core]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
if defined?(Rails)
|
|
2
|
+
require 'rails/generators/base'
|
|
3
|
+
|
|
4
|
+
module Facera
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path('../templates', __FILE__)
|
|
8
|
+
|
|
9
|
+
desc "Install Facera in your Rails application"
|
|
10
|
+
|
|
11
|
+
def create_directories
|
|
12
|
+
empty_directory 'app/cores'
|
|
13
|
+
empty_directory 'app/facets'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_initializer
|
|
17
|
+
create_file 'config/initializers/facera.rb', <<~RUBY
|
|
18
|
+
# Facera configuration
|
|
19
|
+
Facera.configure do |config|
|
|
20
|
+
# Base path for all APIs
|
|
21
|
+
# config.base_path = '/api'
|
|
22
|
+
|
|
23
|
+
# API version
|
|
24
|
+
# config.version = 'v1'
|
|
25
|
+
|
|
26
|
+
# Enable dashboard
|
|
27
|
+
# config.dashboard = true
|
|
28
|
+
|
|
29
|
+
# Custom facet paths
|
|
30
|
+
# config.facet_path :external, '/v1'
|
|
31
|
+
# config.facet_path :internal, '/internal/v1'
|
|
32
|
+
|
|
33
|
+
# Disable specific facets
|
|
34
|
+
# config.disable_facet :agent
|
|
35
|
+
|
|
36
|
+
# Authentication handlers
|
|
37
|
+
# config.authenticate :external do |request|
|
|
38
|
+
# token = request.headers['Authorization']
|
|
39
|
+
# User.find_by_token(token)
|
|
40
|
+
# end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Auto-mount all defined facets
|
|
44
|
+
# This will discover and mount facets from app/facets/
|
|
45
|
+
# Facera.auto_mount!
|
|
46
|
+
RUBY
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def show_readme
|
|
50
|
+
say "\n"
|
|
51
|
+
say "=" * 70
|
|
52
|
+
say "Facera installed successfully!"
|
|
53
|
+
say "=" * 70
|
|
54
|
+
say "\nNext steps:"
|
|
55
|
+
say " 1. Generate a core: rails g facera:core payment"
|
|
56
|
+
say " 2. Generate a facet: rails g facera:facet external --core=payment"
|
|
57
|
+
say " 3. Start your server and visit /api/v1/health"
|
|
58
|
+
say "\nDocumentation: https://github.com/jcagarcia/facera"
|
|
59
|
+
say "=" * 70 + "\n"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# <%= class_name %> Core
|
|
2
|
+
Facera.define_core(:<%= file_name %>) do
|
|
3
|
+
# Define your entity
|
|
4
|
+
entity :<%= entity_name %> do
|
|
5
|
+
attribute :id, :uuid, immutable: true
|
|
6
|
+
attribute :created_at, :timestamp, immutable: true
|
|
7
|
+
attribute :updated_at, :timestamp
|
|
8
|
+
|
|
9
|
+
# Add your attributes here
|
|
10
|
+
# attribute :name, :string, required: true
|
|
11
|
+
# attribute :status, :enum, values: [:active, :inactive]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Define your invariants (business rules)
|
|
15
|
+
# invariant :example_rule, description: "Example invariant" do
|
|
16
|
+
# # Your validation logic
|
|
17
|
+
# true
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
# Define your capabilities
|
|
21
|
+
capability :create_<%= entity_name %>, type: :create do
|
|
22
|
+
entity :<%= entity_name %>
|
|
23
|
+
requires :id
|
|
24
|
+
# Add required params
|
|
25
|
+
# optional :name
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
capability :get_<%= entity_name %>, type: :get do
|
|
29
|
+
entity :<%= entity_name %>
|
|
30
|
+
requires :id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
capability :list_<%= file_name %>, type: :list do
|
|
34
|
+
entity :<%= entity_name %>
|
|
35
|
+
optional :limit, :offset
|
|
36
|
+
# filterable :status
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
capability :update_<%= entity_name %>, type: :update do
|
|
40
|
+
entity :<%= entity_name %>
|
|
41
|
+
requires :id
|
|
42
|
+
# optional :name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
capability :delete_<%= entity_name %>, type: :delete do
|
|
46
|
+
entity :<%= entity_name %>
|
|
47
|
+
requires :id
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# <%= class_name %> Facet
|
|
2
|
+
Facera.define_facet(:<%= file_name %>, core: :<%= core_name %>) do
|
|
3
|
+
description "<%= class_name %> API"
|
|
4
|
+
|
|
5
|
+
# Control field visibility
|
|
6
|
+
# expose :<%= core_name.singularize %> do
|
|
7
|
+
# fields :id, :name, :created_at
|
|
8
|
+
# # hide :sensitive_field
|
|
9
|
+
# # alias_field :created_at, as: :createdAt
|
|
10
|
+
# end
|
|
11
|
+
|
|
12
|
+
# Control capability access
|
|
13
|
+
# allow_capabilities :create_<%= core_name.singularize %>, :get_<%= core_name.singularize %>, :list_<%= core_name %>
|
|
14
|
+
# deny_capabilities :delete_<%= core_name.singularize %>
|
|
15
|
+
|
|
16
|
+
# Add scoping
|
|
17
|
+
# scope :list_<%= core_name %> do
|
|
18
|
+
# { user_id: current_user.id }
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
# Set error verbosity
|
|
22
|
+
error_verbosity :minimal
|
|
23
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'grape'
|
|
2
|
+
|
|
3
|
+
module Facera
|
|
4
|
+
module Grape
|
|
5
|
+
class APIGenerator
|
|
6
|
+
def self.for_facet(facet_name)
|
|
7
|
+
facet = Registry.find_facet(facet_name)
|
|
8
|
+
core = facet.core
|
|
9
|
+
|
|
10
|
+
api_class = Class.new(::Grape::API) do
|
|
11
|
+
version 'v1', using: :header, vendor: 'facera'
|
|
12
|
+
format :json
|
|
13
|
+
content_type :json, 'application/json'
|
|
14
|
+
|
|
15
|
+
# Store facet reference
|
|
16
|
+
define_singleton_method(:facet) { facet }
|
|
17
|
+
|
|
18
|
+
# Add helpers
|
|
19
|
+
helpers do
|
|
20
|
+
def current_user
|
|
21
|
+
# Placeholder - in real implementation this would authenticate
|
|
22
|
+
@current_user ||= { id: 'user-123' }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def current_facet
|
|
26
|
+
self.class.facet
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Generate endpoints for each allowed capability
|
|
31
|
+
facet.allowed_capabilities.each do |capability_name|
|
|
32
|
+
capability = core.find_capability(capability_name)
|
|
33
|
+
next unless capability.entity_name
|
|
34
|
+
|
|
35
|
+
entity_name = capability.entity_name
|
|
36
|
+
resource_name = entity_name.to_s.pluralize
|
|
37
|
+
|
|
38
|
+
# Create resource block
|
|
39
|
+
resource resource_name do
|
|
40
|
+
EndpointGenerator.generate_for(self, capability, facet)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Add a health check endpoint
|
|
45
|
+
get :health do
|
|
46
|
+
{
|
|
47
|
+
status: 'ok',
|
|
48
|
+
facet: facet.name,
|
|
49
|
+
core: core.name,
|
|
50
|
+
timestamp: Time.now.iso8601
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
api_class
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
module Grape
|
|
3
|
+
class EndpointGenerator
|
|
4
|
+
def self.generate_for(api_class, capability, facet)
|
|
5
|
+
case capability.type
|
|
6
|
+
when :create
|
|
7
|
+
generate_create(api_class, capability, facet)
|
|
8
|
+
when :get
|
|
9
|
+
generate_get(api_class, capability, facet)
|
|
10
|
+
when :update
|
|
11
|
+
generate_update(api_class, capability, facet)
|
|
12
|
+
when :delete
|
|
13
|
+
generate_delete(api_class, capability, facet)
|
|
14
|
+
when :list
|
|
15
|
+
generate_list(api_class, capability, facet)
|
|
16
|
+
when :action
|
|
17
|
+
generate_action(api_class, capability, facet)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def self.generate_create(api_class, capability, facet)
|
|
24
|
+
entity_name = capability.entity_name
|
|
25
|
+
resource_name = entity_name.to_s.pluralize
|
|
26
|
+
entity = facet.core.find_entity(entity_name)
|
|
27
|
+
entity_class = EntityGenerator.for(entity, facet)
|
|
28
|
+
|
|
29
|
+
# Pre-calculate param types
|
|
30
|
+
param_types = {}
|
|
31
|
+
capability.required_params.each do |param|
|
|
32
|
+
param_types[param] = param_type_for(param, entity)
|
|
33
|
+
end
|
|
34
|
+
capability.optional_params.each do |param|
|
|
35
|
+
param_types[param] = param_type_for(param, entity)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
api_class.class_eval do
|
|
39
|
+
desc "Create a #{entity_name}" do
|
|
40
|
+
success entity_class
|
|
41
|
+
failure [[400, 'Validation Error'], [401, 'Unauthorized']]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
params do
|
|
45
|
+
capability.required_params.each do |param|
|
|
46
|
+
requires param, type: param_types[param]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
capability.optional_params.each do |param|
|
|
50
|
+
optional param, type: param_types[param]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
post do
|
|
55
|
+
result = Executor.run(
|
|
56
|
+
facet: facet,
|
|
57
|
+
capability: capability,
|
|
58
|
+
params: declared(params),
|
|
59
|
+
context: { current_user: current_user }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
present result, with: entity_class
|
|
63
|
+
rescue Facera::ValidationError => e
|
|
64
|
+
error!({ error: 'validation', errors: e.errors }, 400)
|
|
65
|
+
rescue Facera::UnauthorizedError => e
|
|
66
|
+
error!({ error: 'unauthorized', message: e.message }, 401)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.generate_get(api_class, capability, facet)
|
|
72
|
+
entity_name = capability.entity_name
|
|
73
|
+
resource_name = entity_name.to_s.pluralize
|
|
74
|
+
entity = facet.core.find_entity(entity_name)
|
|
75
|
+
entity_class = EntityGenerator.for(entity, facet)
|
|
76
|
+
|
|
77
|
+
api_class.class_eval do
|
|
78
|
+
desc "Get a #{entity_name}" do
|
|
79
|
+
success entity_class
|
|
80
|
+
failure [[404, 'Not Found'], [401, 'Unauthorized']]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
params do
|
|
84
|
+
requires :id, type: String, desc: "#{entity_name.to_s.capitalize} ID"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
get ':id' do
|
|
88
|
+
result = Executor.run(
|
|
89
|
+
facet: facet,
|
|
90
|
+
capability: capability,
|
|
91
|
+
params: { id: params[:id] },
|
|
92
|
+
context: { current_user: current_user }
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
present result, with: entity_class
|
|
96
|
+
rescue Facera::NotFoundError => e
|
|
97
|
+
error!({ error: 'not_found', message: e.message }, 404)
|
|
98
|
+
rescue Facera::UnauthorizedError => e
|
|
99
|
+
error!({ error: 'unauthorized', message: e.message }, 401)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.generate_list(api_class, capability, facet)
|
|
105
|
+
entity_name = capability.entity_name
|
|
106
|
+
resource_name = entity_name.to_s.pluralize
|
|
107
|
+
entity = facet.core.find_entity(entity_name)
|
|
108
|
+
collection_class = EntityGenerator.for_collection(entity, facet)
|
|
109
|
+
|
|
110
|
+
api_class.class_eval do
|
|
111
|
+
desc "List #{resource_name}" do
|
|
112
|
+
success collection_class
|
|
113
|
+
failure [[401, 'Unauthorized']]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
params do
|
|
117
|
+
optional :limit, type: Integer, default: 20, desc: 'Number of items to return'
|
|
118
|
+
optional :offset, type: Integer, default: 0, desc: 'Number of items to skip'
|
|
119
|
+
|
|
120
|
+
capability.filterable_params.each do |param|
|
|
121
|
+
optional param, type: String, desc: "Filter by #{param}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
capability.optional_params.each do |param|
|
|
125
|
+
next if [:limit, :offset].include?(param)
|
|
126
|
+
optional param, type: String
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
get do
|
|
131
|
+
result = Executor.run(
|
|
132
|
+
facet: facet,
|
|
133
|
+
capability: capability,
|
|
134
|
+
params: declared(params),
|
|
135
|
+
context: { current_user: current_user }
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
present result, with: collection_class
|
|
139
|
+
rescue Facera::UnauthorizedError => e
|
|
140
|
+
error!({ error: 'unauthorized', message: e.message }, 401)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.generate_update(api_class, capability, facet)
|
|
146
|
+
entity_name = capability.entity_name
|
|
147
|
+
entity = facet.core.find_entity(entity_name)
|
|
148
|
+
entity_class = EntityGenerator.for(entity, facet)
|
|
149
|
+
|
|
150
|
+
# Pre-calculate param types
|
|
151
|
+
param_types = {}
|
|
152
|
+
capability.optional_params.each do |param|
|
|
153
|
+
param_types[param] = param_type_for(param, entity)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
api_class.class_eval do
|
|
157
|
+
desc "Update a #{entity_name}" do
|
|
158
|
+
success entity_class
|
|
159
|
+
failure [[400, 'Validation Error'], [404, 'Not Found'], [401, 'Unauthorized']]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
params do
|
|
163
|
+
requires :id, type: String
|
|
164
|
+
|
|
165
|
+
capability.optional_params.each do |param|
|
|
166
|
+
optional param, type: param_types[param]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
patch ':id' do
|
|
171
|
+
result = Executor.run(
|
|
172
|
+
facet: facet,
|
|
173
|
+
capability: capability,
|
|
174
|
+
params: declared(params),
|
|
175
|
+
context: { current_user: current_user }
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
present result, with: entity_class
|
|
179
|
+
rescue Facera::ValidationError => e
|
|
180
|
+
error!({ error: 'validation', errors: e.errors }, 400)
|
|
181
|
+
rescue Facera::NotFoundError => e
|
|
182
|
+
error!({ error: 'not_found', message: e.message }, 404)
|
|
183
|
+
rescue Facera::UnauthorizedError => e
|
|
184
|
+
error!({ error: 'unauthorized', message: e.message }, 401)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.generate_delete(api_class, capability, facet)
|
|
190
|
+
entity_name = capability.entity_name
|
|
191
|
+
|
|
192
|
+
api_class.class_eval do
|
|
193
|
+
desc "Delete a #{entity_name}" do
|
|
194
|
+
success { { success: Boolean, id: String } }
|
|
195
|
+
failure [[404, 'Not Found'], [401, 'Unauthorized']]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
params do
|
|
199
|
+
requires :id, type: String
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
delete ':id' do
|
|
203
|
+
result = Executor.run(
|
|
204
|
+
facet: facet,
|
|
205
|
+
capability: capability,
|
|
206
|
+
params: { id: params[:id] },
|
|
207
|
+
context: { current_user: current_user }
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
present result
|
|
211
|
+
rescue Facera::NotFoundError => e
|
|
212
|
+
error!({ error: 'not_found', message: e.message }, 404)
|
|
213
|
+
rescue Facera::UnauthorizedError => e
|
|
214
|
+
error!({ error: 'unauthorized', message: e.message }, 401)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.generate_action(api_class, capability, facet)
|
|
220
|
+
entity_name = capability.entity_name
|
|
221
|
+
action_name = capability.action_name || extract_action_name(capability.name)
|
|
222
|
+
entity = facet.core.find_entity(entity_name)
|
|
223
|
+
entity_class = EntityGenerator.for(entity, facet)
|
|
224
|
+
|
|
225
|
+
# Pre-calculate param types
|
|
226
|
+
param_types = {}
|
|
227
|
+
capability.required_params.each do |param|
|
|
228
|
+
next if param == :id
|
|
229
|
+
param_types[param] = param_type_for(param, entity)
|
|
230
|
+
end
|
|
231
|
+
capability.optional_params.each do |param|
|
|
232
|
+
param_types[param] = param_type_for(param, entity)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
api_class.class_eval do
|
|
236
|
+
desc "#{action_name.to_s.humanize} a #{entity_name}" do
|
|
237
|
+
success entity_class
|
|
238
|
+
failure [[400, 'Validation Error'], [404, 'Not Found'], [401, 'Unauthorized'], [422, 'Precondition Failed']]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
params do
|
|
242
|
+
requires :id, type: String
|
|
243
|
+
|
|
244
|
+
capability.required_params.each do |param|
|
|
245
|
+
next if param == :id
|
|
246
|
+
requires param, type: param_types[param]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
capability.optional_params.each do |param|
|
|
250
|
+
optional param, type: param_types[param]
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
post ":id/#{action_name}" do
|
|
255
|
+
result = Executor.run(
|
|
256
|
+
facet: facet,
|
|
257
|
+
capability: capability,
|
|
258
|
+
params: declared(params),
|
|
259
|
+
context: { current_user: current_user }
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
present result, with: entity_class
|
|
263
|
+
rescue Facera::ValidationError => e
|
|
264
|
+
error!({ error: 'validation', errors: e.errors }, 400)
|
|
265
|
+
rescue Facera::PreconditionError => e
|
|
266
|
+
error!({ error: 'precondition', message: e.message }, 422)
|
|
267
|
+
rescue Facera::NotFoundError => e
|
|
268
|
+
error!({ error: 'not_found', message: e.message }, 404)
|
|
269
|
+
rescue Facera::UnauthorizedError => e
|
|
270
|
+
error!({ error: 'unauthorized', message: e.message }, 401)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def self.extract_action_name(capability_name)
|
|
276
|
+
# Extract action from capability name like "confirm_payment" -> "confirm"
|
|
277
|
+
name_parts = capability_name.to_s.split('_')
|
|
278
|
+
name_parts[0...-1].join('_').to_sym
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def self.param_type_for(param_name, entity)
|
|
282
|
+
attr = entity.find_attribute(param_name)
|
|
283
|
+
return String unless attr
|
|
284
|
+
|
|
285
|
+
case attr.type
|
|
286
|
+
when :string, :uuid then String
|
|
287
|
+
when :integer then Integer
|
|
288
|
+
when :float, :money then Float
|
|
289
|
+
when :boolean then Boolean
|
|
290
|
+
when :hash then Hash
|
|
291
|
+
when :array then Array
|
|
292
|
+
else String
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Add humanize method if not available
|
|
300
|
+
unless String.method_defined?(:humanize)
|
|
301
|
+
class String
|
|
302
|
+
def humanize
|
|
303
|
+
self.gsub('_', ' ').capitalize
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Add pluralize method if not available
|
|
309
|
+
unless String.method_defined?(:pluralize)
|
|
310
|
+
class String
|
|
311
|
+
def pluralize
|
|
312
|
+
return self if self.end_with?('s')
|
|
313
|
+
"#{self}s"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|