democritus 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.hound.yml +107 -0
- data/.travis.yml +7 -0
- data/README.md +59 -1
- data/Rakefile +31 -0
- data/democritus.gemspec +9 -1
- data/lib/democritus.rb +18 -0
- data/lib/democritus/class_builder.rb +156 -5
- data/lib/democritus/class_builder/command.rb +36 -0
- data/lib/democritus/class_builder/commands.rb +10 -0
- data/lib/democritus/class_builder/commands/attribute.rb +33 -0
- data/lib/democritus/class_builder/commands/attributes.rb +51 -0
- data/lib/democritus/from_json_class_builder.rb +96 -0
- data/lib/democritus/version.rb +4 -1
- metadata +110 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8db17897f669a971aa5b22ba819978953a3eda10
|
4
|
+
data.tar.gz: d3f9514bb722c2dd6eb51a4beb06d0e48e606524
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22a3ec1e45a1e3dce5cb8c36751723c88a37002d6a654e7ac7899a0dd926a5f3657f422592e5e94795df8cf1235e1bc6023a277dcef638445dbd99cbfeb1c3c3
|
7
|
+
data.tar.gz: d34e1f0f3288fa495664c7c63710185ee1a9eb7ae2a61591a3e50b85c8a2b561e67ac5f5184cefcf3bca1bb44a7508dbc3de949fec934943d1159c8be8b4810a
|
data/.hound.yml
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
################################################################################
|
3
|
+
## Releasing the hounds in your local environment.
|
4
|
+
##
|
5
|
+
## Setup:
|
6
|
+
## $ gem install rubocop
|
7
|
+
##
|
8
|
+
## Run:
|
9
|
+
## $ rubocop ./path/to/file ./or/path/to/directory -c ./.hound.yml
|
10
|
+
##
|
11
|
+
################################################################################
|
12
|
+
AllCops:
|
13
|
+
Include:
|
14
|
+
- Rakefile
|
15
|
+
Exclude:
|
16
|
+
- app/data_generators/sipity/data_generators/**/*
|
17
|
+
- db/**/*
|
18
|
+
- bin/**/*
|
19
|
+
- config/**/*
|
20
|
+
- dragonfly/**/*
|
21
|
+
- 'spec/fixtures/**/*'
|
22
|
+
- 'vendor/**/*'
|
23
|
+
- 'scripts/**/*'
|
24
|
+
- 'tmp/**/*'
|
25
|
+
- 'spec/support/sipity/command_repository_interface.rb'
|
26
|
+
- 'spec/support/sipity/query_repository_interface.rb'
|
27
|
+
- 'app/validators/open_for_starting_submissions_validator.rb'
|
28
|
+
- 'app/forms/sipity/forms/form_builder.rb'
|
29
|
+
RunRailsCops: false
|
30
|
+
|
31
|
+
LineLength:
|
32
|
+
Description: 'Limit lines to 140 characters.'
|
33
|
+
Max: 140
|
34
|
+
Enabled: true
|
35
|
+
|
36
|
+
AlignParameters:
|
37
|
+
Description: >-
|
38
|
+
Align the parameters of a method call if they span more
|
39
|
+
than one line.
|
40
|
+
Enabled: true
|
41
|
+
|
42
|
+
CyclomaticComplexity:
|
43
|
+
Description: 'Avoid complex methods.'
|
44
|
+
Enabled: true
|
45
|
+
Exclude:
|
46
|
+
|
47
|
+
Documentation:
|
48
|
+
Description: 'Document classes and non-namespace modules.'
|
49
|
+
Enabled: true
|
50
|
+
Exclude:
|
51
|
+
- spec/**/*
|
52
|
+
- lib/**/version.rb
|
53
|
+
|
54
|
+
Metrics/PerceivedComplexity:
|
55
|
+
Enabled: true
|
56
|
+
Exclude:
|
57
|
+
|
58
|
+
Metrics/AbcSize:
|
59
|
+
Enabled: true
|
60
|
+
Max: 12
|
61
|
+
Exclude:
|
62
|
+
|
63
|
+
Delegate:
|
64
|
+
Description: 'Prefer delegate method for delegations.'
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
EmptyLinesAroundBlockBody:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
DotPosition:
|
71
|
+
Description: 'Checks the position of the dot in multi-line method calls.'
|
72
|
+
EnforcedStyle: trailing
|
73
|
+
Enabled: true
|
74
|
+
|
75
|
+
Style/Encoding:
|
76
|
+
Description: 'Use UTF-8 as the source file encoding.'
|
77
|
+
Enabled: false
|
78
|
+
|
79
|
+
FileName:
|
80
|
+
Description: 'Use snake_case for source file names.'
|
81
|
+
Enabled: true
|
82
|
+
|
83
|
+
PercentLiteralDelimiters:
|
84
|
+
Description: 'Use `%`-literal delimiters consistently'
|
85
|
+
PreferredDelimiters:
|
86
|
+
'%': ()
|
87
|
+
'%i': ()
|
88
|
+
'%q': ()
|
89
|
+
'%Q': ()
|
90
|
+
'%r': '{}'
|
91
|
+
'%s': ()
|
92
|
+
'%w': ()
|
93
|
+
'%W': ()
|
94
|
+
'%x': ()
|
95
|
+
Enabled: true
|
96
|
+
|
97
|
+
RedundantReturn:
|
98
|
+
Description: "Don't use return where it's not required."
|
99
|
+
Enabled: false
|
100
|
+
|
101
|
+
StringLiterals:
|
102
|
+
Description: 'Checks if uses of quotes match the configured preference.'
|
103
|
+
Enabled: false
|
104
|
+
|
105
|
+
WordArray:
|
106
|
+
Description: 'Use %w or %W for arrays of words.'
|
107
|
+
Enabled: false
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,3 +1,61 @@
|
|
1
1
|
# Democritus
|
2
2
|
|
3
|
-
|
3
|
+
[![Build Status](https://travis-ci.org/jeremyf/democritus.png?branch=master)](https://travis-ci.org/jeremyf/democritus)
|
4
|
+
[![APACHE 2 License](http://img.shields.io/badge/APACHE2-license-blue.svg)](./LICENSE)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/jeremyf/democritus.png)](https://codeclimate.com/github/jeremyf/democritus)
|
6
|
+
[![Test Coverage](https://codeclimate.com/github/jeremyf/democritus/badges/coverage.svg)](https://codeclimate.com/github/jeremyf/democritus)
|
7
|
+
[![Documentation Status](http://inch-ci.org/github/jeremyf/democritus.svg?branch=master)](http://inch-ci.org/github/jeremyf/democritus)
|
8
|
+
|
9
|
+
Democritus is a plugin for building class from reusable components.
|
10
|
+
|
11
|
+
Democritus is inspired as followup of a common pattern that I saw in the development of [Sipity's](https://github.com/ndlib/sipity/) form objects.
|
12
|
+
It also aims to address the needs of Sipity's yet to be developed sibling application; The dissemination of processed data.
|
13
|
+
|
14
|
+
I'm looking to apply the ideas put forward in Avdi Grimm's [Naught gem](https://github.com/avdi/naught).
|
15
|
+
|
16
|
+
## Goal
|
17
|
+
|
18
|
+
I would like to be able to declare in Ruby the following:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
ApprovalForm = Democritus.build(command_namespaces: ['Sipity::DemocritusCommands', 'Democritus::ClassBuilder::Commands']) do |builder|
|
22
|
+
builder.form do
|
23
|
+
attributes do
|
24
|
+
attribute(name: 'agree_to_terms_of_service', type: 'Boolean', validates: 'acceptance')
|
25
|
+
end
|
26
|
+
action_name(name: 'approval')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
With an `ApprovalForm`, I could `#submit` if `#valid?` (i.e. the `agree_to_terms_of_service` has been accepted).
|
32
|
+
|
33
|
+
From that point forward, I would like to be able to create the class based on a JSON description:
|
34
|
+
|
35
|
+
```json
|
36
|
+
{
|
37
|
+
"#command_namespaces": ["Sipity::DemocritusCommands", "Democritus::ClassBuilder::Commands"],
|
38
|
+
"#form": {
|
39
|
+
"#attributes": {
|
40
|
+
"#attribute": [
|
41
|
+
{ "name": "agree_to_terms_of_service", "type": "Boolean", "validates": "acceptance" }
|
42
|
+
]
|
43
|
+
},
|
44
|
+
"#action_name": { "name": "approval" }
|
45
|
+
}
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
ApprovalForm = Demcritus.build_from_json(json)
|
51
|
+
```
|
52
|
+
|
53
|
+
## Roadmap
|
54
|
+
|
55
|
+
- [x] Rudimentary plugin command behavior
|
56
|
+
- [x] [Command::Attribute](./lib/democritus/class_builder/commands/attribute.rb)
|
57
|
+
- [x] [Command::Attirubtes](./lib/democritus/class_builder/commands/attributes.rb)
|
58
|
+
- [ ] Create the commands that build a Sipity processing form
|
59
|
+
- [X] Build class from JSON configuration
|
60
|
+
- [ ] Basic case for nested commands
|
61
|
+
- [ ] Allow for "constantization" of command_namespaces option.
|
data/Rakefile
CHANGED
@@ -1 +1,32 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
unless Rake::Task.task_defined?('spec')
|
4
|
+
begin
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
7
|
+
t.pattern = "./spec/**/*_spec.rb"
|
8
|
+
ENV['COVERAGE'] = 'true'
|
9
|
+
end
|
10
|
+
rescue LoadError
|
11
|
+
$stdout.puts "RSpec failed to load; You won't be able to run tests."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'rubocop/rake_task'
|
16
|
+
RuboCop::RakeTask.new do |task|
|
17
|
+
task.requires << 'rubocop-rspec'
|
18
|
+
task.options << "--config=.hound.yml"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'reek/rake/task'
|
22
|
+
Reek::Rake::Task.new do |task|
|
23
|
+
task.verbose = true
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'flay_task'
|
27
|
+
FlayTask.new do |task|
|
28
|
+
task.verbose = true
|
29
|
+
task.threshold = 20
|
30
|
+
end
|
31
|
+
|
32
|
+
task(default: ['rubocop', 'reek', 'flay', 'spec'])
|
data/democritus.gemspec
CHANGED
@@ -17,9 +17,17 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.bindir = "exe"
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
|
+
spec.required_ruby_version = '~> 2.1'
|
20
21
|
|
21
|
-
spec.add_development_dependency "bundler", "~> 1.
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
24
|
spec.add_development_dependency "rspec", "~> 3.2"
|
24
25
|
spec.add_development_dependency "rspec-its", "~> 1.2"
|
26
|
+
spec.add_development_dependency "reek"
|
27
|
+
spec.add_development_dependency "flay"
|
28
|
+
spec.add_development_dependency "rubocop"
|
29
|
+
spec.add_development_dependency "rubocop-rspec"
|
30
|
+
spec.add_development_dependency "simplecov"
|
31
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
32
|
+
spec.add_development_dependency "byebug"
|
25
33
|
end
|
data/lib/democritus.rb
CHANGED
@@ -1,16 +1,34 @@
|
|
1
1
|
require "democritus/version"
|
2
2
|
require 'democritus/class_builder'
|
3
|
+
require 'democritus/class_builder/commands'
|
4
|
+
require 'democritus/from_json_class_builder'
|
3
5
|
|
6
|
+
# Compose objects by leveraging a DSL for class creation.
|
7
|
+
# Yes, we can write code that conforms to interfaces, but in my experience, as the Ruby object ecosystem has grown, so too has the needs
|
8
|
+
# for understanding the galaxy of objects.
|
4
9
|
module Democritus
|
5
10
|
# @api public
|
6
11
|
#
|
7
12
|
# Responsible for building a class based on atomic components.
|
13
|
+
#
|
14
|
+
# @yield [Democritus::ClassBuilder] Gives a builder to provide additional command style customizations
|
15
|
+
# @return Class
|
8
16
|
def self.build(&configuration_block)
|
9
17
|
builder = ClassBuilder.new
|
10
18
|
builder.customize(&configuration_block)
|
11
19
|
builder.generate_class
|
12
20
|
end
|
13
21
|
|
22
|
+
# @api public
|
23
|
+
#
|
24
|
+
# Responsible for building a class based on the given JSON object.
|
25
|
+
#
|
26
|
+
# @return Class
|
27
|
+
def self.build_from_json(json)
|
28
|
+
builder = FromJsonClassBuilder.new(json)
|
29
|
+
builder.generate_class
|
30
|
+
end
|
31
|
+
|
14
32
|
# An empty module intended to be exposed for is_a? comparisons (and ==)
|
15
33
|
#
|
16
34
|
# @example
|
@@ -1,16 +1,167 @@
|
|
1
1
|
module Democritus
|
2
|
+
# Responsible for building a class based on the customization's applied
|
3
|
+
# through the #customize method.
|
4
|
+
#
|
5
|
+
# @see ./spec/lib/democritus/class_builder_spec.rb
|
6
|
+
#
|
7
|
+
# :reek:UnusedPrivateMethod: { exclude: [ !ruby/regexp /(method_missing|respond_to_missing)/ ] }
|
2
8
|
class ClassBuilder
|
3
|
-
# @
|
4
|
-
def
|
5
|
-
|
9
|
+
# @param command_namespaces [Array<Module>] the sequential list of namespaces you want to check for each registered command.
|
10
|
+
def initialize(command_namespaces: default_command_namespaces)
|
11
|
+
self.customization_module = Module.new
|
12
|
+
self.generation_module = Module.new
|
13
|
+
self.class_operations = []
|
14
|
+
self.instance_operations = []
|
15
|
+
self.command_namespaces = command_namespaces
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# The module that receives customized method definitions.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# Democritus.build do |builder|
|
24
|
+
# builder.a_command
|
25
|
+
# def a_customization
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# The above #a_customization method is captured in the customization_module and applied as an instance method
|
30
|
+
# to the generated class.
|
31
|
+
attr_accessor :customization_module
|
32
|
+
attr_accessor :generation_module
|
33
|
+
|
34
|
+
# Command operations to be applied as class methods of the generated_class.
|
35
|
+
attr_accessor :class_operations
|
36
|
+
|
37
|
+
def default_command_namespaces
|
38
|
+
[Democritus::ClassBuilder::Commands]
|
39
|
+
end
|
40
|
+
|
41
|
+
# The command namespaces that you want to use. Note, order is important
|
42
|
+
attr_reader :command_namespaces
|
43
|
+
|
44
|
+
def command_namespaces=(input)
|
45
|
+
@command_namespaces = Array(input)
|
6
46
|
end
|
7
47
|
|
48
|
+
# Command operations to be applied as instance methods of the generated_class.
|
49
|
+
attr_accessor :instance_operations
|
50
|
+
|
51
|
+
public
|
52
|
+
|
8
53
|
# @api public
|
9
54
|
#
|
10
|
-
# Responsible for
|
11
|
-
#
|
55
|
+
# Responsible for executing the customization block against the
|
56
|
+
# customization module with the builder class as a parameter.
|
57
|
+
#
|
58
|
+
# @yield [Democritus::ClassBuilder] the means to build your custom class.
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# ClassBuilder.new.customize do |builder|
|
62
|
+
# builder.command('paramter')
|
63
|
+
# def to_s; 'parameter'; end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @return nil
|
67
|
+
# @see ./spec/lib/democritus/class_builder_spec.rb
|
12
68
|
def customize(&customization_block)
|
13
69
|
return unless customization_block
|
70
|
+
customization_module.module_exec(self, &customization_block)
|
71
|
+
return nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# @api public
|
75
|
+
#
|
76
|
+
# Responsible for generating a Class object based on the customizations
|
77
|
+
# applied via a customize block.
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# dynamic_class = Democritus::ClassBuilder.new.generate_class
|
81
|
+
# an_instance_of_the_dynamic_class = dynamic_class.new
|
82
|
+
#
|
83
|
+
# @return Class object
|
84
|
+
#
|
85
|
+
# rubocop:disable MethodLength
|
86
|
+
# :reek:TooManyStatements: { exclude: [ 'Democritus::ClassBuilder#generate_class' ] }
|
87
|
+
def generate_class
|
88
|
+
generation_mod = generation_module # get a local binding
|
89
|
+
customization_mod = customization_module # get a local binding
|
90
|
+
apply_operations(instance_operations, generation_mod)
|
91
|
+
generated_class = Class.new do
|
92
|
+
const_set :GeneratedMethods, generation_mod
|
93
|
+
const_set :Customizations, customization_mod
|
94
|
+
include DemocritusObjectTag
|
95
|
+
include generation_mod
|
96
|
+
include customization_mod
|
97
|
+
end
|
98
|
+
generated_class
|
99
|
+
end
|
100
|
+
# rubocop:enable MethodLength
|
101
|
+
|
102
|
+
def defer(options = {}, &deferred_operation)
|
103
|
+
if options[:prepend]
|
104
|
+
instance_operations.unshift(deferred_operation)
|
105
|
+
else
|
106
|
+
instance_operations << deferred_operation
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
# :reek:UtilityFunction: { exclude: [ 'Democritus::ClassBuilder#apply_operations' ] }
|
113
|
+
def apply_operations(operations, module_or_class)
|
114
|
+
operations.each do |operation|
|
115
|
+
operation.call(module_or_class)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @!group Method Missing
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# @api public
|
124
|
+
def method_missing(method_name, *args, **kargs, &block)
|
125
|
+
command_name = self.class.command_name_for_method(method_name)
|
126
|
+
command_namespace = command_namespace_for(command_name)
|
127
|
+
if command_namespace
|
128
|
+
command_class = command_namespace.const_get(command_name)
|
129
|
+
command_class.new(*args, **kargs.merge(builder: self), &block).call
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# @api public
|
136
|
+
def respond_to_missing?(method_name, *args)
|
137
|
+
respond_to_definition(method_name, :respond_to_missing?, *args)
|
138
|
+
end
|
139
|
+
|
140
|
+
# @api public
|
141
|
+
def respond_to_definition(method_name, *)
|
142
|
+
command_name = self.class.command_name_for_method(method_name)
|
143
|
+
command_namespace_for(command_name)
|
144
|
+
end
|
145
|
+
|
146
|
+
# @api private
|
147
|
+
def command_namespace_for(command_name)
|
148
|
+
command_namespaces.detect { |cs| cs.const_defined?(command_name) }
|
149
|
+
end
|
150
|
+
# @!endgroup
|
151
|
+
|
152
|
+
class << self
|
153
|
+
# @api public
|
154
|
+
#
|
155
|
+
# Convert the given :method_name into a "constantized" method name.
|
156
|
+
#
|
157
|
+
# @example
|
158
|
+
# Democritus::ClassBuilder.command_name_for_method(:test_command) == 'TestCommand'
|
159
|
+
#
|
160
|
+
# @param method_name [#to_s]
|
161
|
+
# @return String
|
162
|
+
def command_name_for_method(method_name)
|
163
|
+
method_name.to_s.gsub(/(?:^|_)([a-z])/) { Regexp.last_match[1].upcase }
|
164
|
+
end
|
14
165
|
end
|
15
166
|
end
|
16
167
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Democritus
|
2
|
+
class ClassBuilder
|
3
|
+
# @api public
|
4
|
+
#
|
5
|
+
# An abstract class useful in composing additional Democritus::Commands
|
6
|
+
#
|
7
|
+
# The expected interface for a Democritus::Command is as follows:
|
8
|
+
#
|
9
|
+
# * Its #initialize method must accept a :builder keyword (i.e. `#initialize`)
|
10
|
+
# * It responds to #call and #call does not accept any parameters
|
11
|
+
class Command
|
12
|
+
# @api public
|
13
|
+
#
|
14
|
+
# @param builder [Democritus::ClassBuilder] The context in which we are leveraging this building command.
|
15
|
+
def initialize(*, builder:)
|
16
|
+
self.builder = builder
|
17
|
+
end
|
18
|
+
|
19
|
+
# @api public
|
20
|
+
#
|
21
|
+
# @abstract Subclass and override #call to implement
|
22
|
+
def call
|
23
|
+
fail(NotImplementedError, 'Method #call should be overriden in child classes')
|
24
|
+
end
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
def defer(options = {}, &block)
|
28
|
+
builder.defer(options, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_accessor :builder
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Democritus
|
2
|
+
# A container namespace for Democritus commands that are leveraged for `Democritus.build`
|
3
|
+
#
|
4
|
+
# @see Democritus::Command for interface of each command.
|
5
|
+
module Commands
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'democritus/class_builder/commands/attribute'
|
10
|
+
require 'democritus/class_builder/commands/attributes'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'democritus/class_builder/command'
|
2
|
+
|
3
|
+
module Democritus
|
4
|
+
class ClassBuilder
|
5
|
+
module Commands
|
6
|
+
# Command to assign an attribute to the given built class.
|
7
|
+
class Attribute < ::Democritus::ClassBuilder::Command
|
8
|
+
def initialize(name:, **options)
|
9
|
+
self.builder = options.fetch(:builder)
|
10
|
+
self.name = name
|
11
|
+
self.options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name, :options
|
15
|
+
|
16
|
+
# :reek:NestedIterators: { exclude: [ 'Democritus::ClassBuilder::Commands::Attribute#call' ] }
|
17
|
+
def call
|
18
|
+
defer do |subject|
|
19
|
+
subject.module_exec(@name) do |name|
|
20
|
+
attr_reader name
|
21
|
+
private
|
22
|
+
attr_writer name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_writer :name, :options
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'democritus/class_builder/command'
|
2
|
+
|
3
|
+
module Democritus
|
4
|
+
class ClassBuilder
|
5
|
+
module Commands
|
6
|
+
# Command to assign attributes as part of the initialize method.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# Democritus::ClassBuilder::Commands::Attributes.new(builder: a_builder) do
|
10
|
+
# attribute(:name)
|
11
|
+
# attribute(:coolness_factor)
|
12
|
+
# end
|
13
|
+
class Attributes < ::Democritus::ClassBuilder::Command
|
14
|
+
# @param builder [Democritus::ClassBuilder]
|
15
|
+
# @param additional_configuration [Proc] A means to nest additional configuration
|
16
|
+
def initialize(builder:, &additional_configuration)
|
17
|
+
self.builder = builder
|
18
|
+
self.additional_configuration = additional_configuration
|
19
|
+
self.attribute_names = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# :reek:NestedIterators: { exclude: [ 'Democritus::ClassBuilder::Commands::Attributes#call' ] }
|
23
|
+
# :reek:TooManyStatements: { exclude: [ 'Democritus::ClassBuilder::Commands::Attributes#call' ] }
|
24
|
+
def call
|
25
|
+
# It may seem a little odd to yield self via an instance_exec, however in some cases I need a
|
26
|
+
# receiver for messages (i.e. FromJsonClassBuilder)
|
27
|
+
instance_exec(self, &additional_configuration) if additional_configuration.respond_to?(:call)
|
28
|
+
defer do |subject|
|
29
|
+
subject.module_exec(@attribute_names) do |attribute_names|
|
30
|
+
define_method(:initialize) do |**attributes|
|
31
|
+
attribute_names.each do |attribute_name|
|
32
|
+
send("#{attribute_name}=", attributes.fetch(attribute_name.to_sym, nil))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def attribute(name:, **options)
|
40
|
+
name = name.to_sym
|
41
|
+
attribute_names << name
|
42
|
+
builder.attribute(name: name, **options)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_accessor :additional_configuration, :attribute_names
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'json'
|
2
|
+
module Democritus
|
3
|
+
# Responsible for building a class based on the given JSON document.
|
4
|
+
#
|
5
|
+
# Note the following structure:
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# ```json
|
9
|
+
# { "#command_name": { "keyword_param_one": "param_value", "#nested_command_name": { "nested_keyword_param": "nested_param_value"} } }
|
10
|
+
# ```
|
11
|
+
#
|
12
|
+
# Commands that are called against the builder are Hash keys that start with '#'. Keywords are command parameters that
|
13
|
+
# do not start with '#'.
|
14
|
+
#
|
15
|
+
# @note This is a class with a greater "reek" than I would like. However, it
|
16
|
+
# is parsing JSON and loading that into ruby; Its complicated. So I'm
|
17
|
+
# willing to accept and assume responsibility for this code "reek".
|
18
|
+
#
|
19
|
+
# @see Democritus::ClassBuilder::Commands
|
20
|
+
# @see Democritus::FromJsonClassBuilder::KEY_IS_COMMAND_REGEXP
|
21
|
+
class FromJsonClassBuilder
|
22
|
+
# @api public
|
23
|
+
#
|
24
|
+
# @param json_document [String] A JSON document
|
25
|
+
def initialize(json_document)
|
26
|
+
self.data = json_document
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :data
|
32
|
+
|
33
|
+
def data=(json_document)
|
34
|
+
@data = JSON.parse(json_document)
|
35
|
+
end
|
36
|
+
|
37
|
+
public
|
38
|
+
|
39
|
+
# @api public
|
40
|
+
#
|
41
|
+
# A wrapper around the Democritus::ClassBuilder#generate_class. However instead of evaulating blocks, the builder must
|
42
|
+
# be called directly.
|
43
|
+
#
|
44
|
+
# @return Class object
|
45
|
+
def generate_class
|
46
|
+
keywords, nested_commands = extract_keywords_and_nested_commands(node: data)
|
47
|
+
class_builder = ClassBuilder.new(**keywords)
|
48
|
+
build(node: nested_commands, class_builder: class_builder)
|
49
|
+
class_builder.generate_class
|
50
|
+
end
|
51
|
+
|
52
|
+
KEY_IS_COMMAND_REGEXP = /\A\#(.+)$/.freeze
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# rubocop:disable MethodLength
|
57
|
+
# :reek:TooManyStatements: { exclude: [ 'Democritus::FromJsonClassBuilder#extract_keywords_and_nested_commands' ] }
|
58
|
+
# :reek:UtilityFunction: { exclude: [ 'Democritus::FromJsonClassBuilder#extract_keywords_and_nested_commands' ] }
|
59
|
+
def extract_keywords_and_nested_commands(node:)
|
60
|
+
keywords = {}
|
61
|
+
options = {}
|
62
|
+
node.each_pair do |key, value|
|
63
|
+
match_data = KEY_IS_COMMAND_REGEXP.match(key)
|
64
|
+
if match_data
|
65
|
+
options[match_data[1].to_sym] = value
|
66
|
+
else
|
67
|
+
keywords[key.to_sym] = value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
return [keywords, options]
|
71
|
+
end
|
72
|
+
# rubocop:enable MethodLength
|
73
|
+
|
74
|
+
# :reek:NestedIterators: { exclude: [ 'Democritus::FromJsonClassBuilder#build' ] }
|
75
|
+
def build(node:, class_builder:)
|
76
|
+
json_class_builder = self # establishing local binding
|
77
|
+
each_command(node: node) do |command_name, keywords, nested_commands|
|
78
|
+
class_builder.public_send(command_name, **keywords) do |nested_builder|
|
79
|
+
json_class_builder.send(:build, node: nested_commands, class_builder: nested_builder)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# :reek:NestedIterators: { exclude: [ 'Democritus::FromJsonClassBuilder#each_command' ] }
|
85
|
+
# :reek:FeatureEnvy: { exclude: [ 'Democritus::FromJsonClassBuilder#each_command' ] }
|
86
|
+
def each_command(node:)
|
87
|
+
node.each_pair do |command_name, nested_nodes|
|
88
|
+
nested_nodes = [nested_nodes] unless nested_nodes.is_a?(Array)
|
89
|
+
nested_nodes.each do |nested_node|
|
90
|
+
keywords, nested_commands = extract_keywords_and_nested_commands(node: nested_node)
|
91
|
+
yield(command_name, keywords, nested_commands)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/democritus/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: democritus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Friesen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.6'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
26
|
+
version: '1.6'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +66,104 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: reek
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: flay
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: codeclimate-test-reporter
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: byebug
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
69
167
|
description: A placeholder for an attribute declaration mechanism
|
70
168
|
email:
|
71
169
|
- jeremy.n.friesen@gmail.com
|
@@ -74,6 +172,7 @@ extensions: []
|
|
74
172
|
extra_rdoc_files: []
|
75
173
|
files:
|
76
174
|
- ".gitignore"
|
175
|
+
- ".hound.yml"
|
77
176
|
- ".travis.yml"
|
78
177
|
- Gemfile
|
79
178
|
- LICENSE
|
@@ -84,6 +183,11 @@ files:
|
|
84
183
|
- democritus.gemspec
|
85
184
|
- lib/democritus.rb
|
86
185
|
- lib/democritus/class_builder.rb
|
186
|
+
- lib/democritus/class_builder/command.rb
|
187
|
+
- lib/democritus/class_builder/commands.rb
|
188
|
+
- lib/democritus/class_builder/commands/attribute.rb
|
189
|
+
- lib/democritus/class_builder/commands/attributes.rb
|
190
|
+
- lib/democritus/from_json_class_builder.rb
|
87
191
|
- lib/democritus/version.rb
|
88
192
|
homepage: https://github.com/jeremyf/democritus
|
89
193
|
licenses: []
|
@@ -94,9 +198,9 @@ require_paths:
|
|
94
198
|
- lib
|
95
199
|
required_ruby_version: !ruby/object:Gem::Requirement
|
96
200
|
requirements:
|
97
|
-
- - "
|
201
|
+
- - "~>"
|
98
202
|
- !ruby/object:Gem::Version
|
99
|
-
version: '
|
203
|
+
version: '2.1'
|
100
204
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
205
|
requirements:
|
102
206
|
- - ">="
|