democritus 0.1.0 → 0.2.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 +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
|
+
[](https://travis-ci.org/jeremyf/democritus)
|
4
|
+
[](./LICENSE)
|
5
|
+
[](https://codeclimate.com/github/jeremyf/democritus)
|
6
|
+
[](https://codeclimate.com/github/jeremyf/democritus)
|
7
|
+
[](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
|
- - ">="
|