jeny 0.1.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f302cd633c4386fa74ad685168b0a8676c74dd70a76c93fc2f7403e5dc6d912b
4
- data.tar.gz: fff5a77a0735598d92caa0d5c520fbcded3a5cc327717f54a373bb31010603f5
3
+ metadata.gz: 80c63bb3ba6b2c4dbb710eb1b465427e207663c56d6f03872871ceb999fd01b8
4
+ data.tar.gz: 99188a6695c03b3520c0a8b03b499125ac8ba8ff9989114b8520b29e372b0652
5
5
  SHA512:
6
- metadata.gz: b8276ec8e77f88ae26605647e2452976a86b4ede04d18bd626d2ed3574284d994869715d4df7fcc848529c90b0dd0f6c1579211341b7d1618ca4b16f30926eca
7
- data.tar.gz: 197fa5b18e11edfb337245e0aa26511150cc0e68415b5004756d13a0b42695885ccf3dd229196cb67d58c8abd5d208b40d40f32cf7bcc4d36ee53064126d4933
6
+ metadata.gz: dd518b08c9d0e212dbd185552c8acc3e328c727e4a774198c15b9dfdafef2fa8a80103bac09c3c5432b387560450f01ded4b472667b525953cfffce7afc8694f
7
+ data.tar.gz: 68caadbe294ce2da79a680ee008cbda7d18b0bd2bb2adce536e04c887a96be04dc3be9b6ef685605196077b379c67d252f160079c3172bbc9a360a715426bdab
data/Gemfile CHANGED
@@ -1,5 +1,2 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
-
4
- # gem "predicate", github: "enspirit/predicate", branch: "placeholders"
5
- # gem "predicate", path: "../predicate"
data/README.md CHANGED
@@ -1,7 +1,198 @@
1
1
  # Jeny
2
2
 
3
- Jeny aims at being a simple yet powerful code generation and scaffolding
4
- system.
3
+ ![](https://travis-ci.com/enspirit/jeny.svg?branch=master)
4
+
5
+ Jeny is a simple yet powerful commandline tool for scaffolding new
6
+ projects and generating code snippets in existing ones.
7
+
8
+ ```sh
9
+ jeny --help
10
+ ```
11
+
12
+ The outline of this readme is as follows:
13
+
14
+ - Two use cases: scaffolding & snippets
15
+ - The templating language
16
+ - How do I generate dynamic file names?
17
+ - Use `--git` for atomic generation of snippets
18
+ - Configuring `jeny`: env vars, .jeny file and commandline options
19
+ - Contributing, Licence, etc.
20
+
21
+ ## Generate a project from a scaffold
22
+
23
+ The first use case is the generation of a project structure from
24
+ an existing scaffold.
25
+
26
+ ```sh
27
+ jeny --[no-]edit -d ... -d ... generate path/to/scaffold path/to/target
28
+ ```
29
+
30
+ This command will recursively copy and instantiate files and
31
+ directories from the scaffold folder to the destination folder.
32
+ Your favorite editor opens on files having at least one TODO.
33
+
34
+ File content is generated using a very simple and not that
35
+ powerful templating language, see sections later. Code snippets
36
+ of the following section are NOT supported for now.
37
+
38
+ ## Generate code snippets on an existing project
39
+
40
+ The second use case is the generation of code snippets inside
41
+ existing annotated source code.
42
+
43
+ ```sh
44
+ jeny --[no-]edit -d ... -d ... snippet snipname path/to/code
45
+ ```
46
+
47
+ Code snippets are commented code blocks prefixed by a jeny delimiter.
48
+ When executing `jeny s`, all files under `path/to/files` are inspected
49
+ and jeny code blocks instantiated as uncommented code. Your favorite
50
+ editor opens on files having at least one TODO.
51
+
52
+ For instance, when executing
53
+
54
+ ```sh
55
+ jeny -d name:hello snippet method .
56
+ ```
57
+
58
+ the following code block...
59
+
60
+ ```
61
+ #jeny(method) def ${name}
62
+ #jeny(method) # TODO
63
+ #jeny(method) end
64
+ ```
65
+
66
+ ... will generate the code below in the file where it is found,
67
+ while preserving the jeny code block for subsequent calls:
68
+
69
+ ```
70
+ def hello
71
+ # TODO
72
+ end
73
+ ```
74
+
75
+ The `s[snippet]` command is also able to generate fresh new files,
76
+ provided they end with `.jeny` and have a clearly snippet identifier
77
+ as first line. For instance, a file called `test.rb.jeny` will be
78
+ instantiated when generating a `method` snippet if it contains the
79
+ following first line:
80
+
81
+ ```
82
+ #jeny(method)
83
+ ```
84
+
85
+ ## Templating language
86
+
87
+ The current template language uses [WLang](https://github.com/blambeau/wlang)
88
+ with a very simple dialect. Jeny only supports simple variable for now. It
89
+ does not support iterations and conditionals, on intent (?).
90
+
91
+ Let's say you specify a `-d op_name:my_method` commandline option, the
92
+ following casing tags are recognized:
93
+
94
+ ```
95
+ ${op_name} -> my_method (snake)
96
+ ${opname} -> mymethod (flat)
97
+ ${opName} -> myMethod (camel)
98
+ ${OpName} -> MyMethod (pascal)
99
+ ${OP_NAME} -> MY_METHOD (screaming)
100
+ ${OP NAME} -> MYMETHOD (upper)
101
+ ${OPNAME} -> MYMETHOD (upper flat)
102
+ ${op-name} -> my-method (kebab)
103
+ ${Op-Name} -> My-Method (header)
104
+ ${OP-NAME} -> MY-METHOD (cobol)
105
+ ${OP|NAME} -> MY|METHOD (donner)
106
+ ```
107
+
108
+ ## Dynamic file names
109
+
110
+ If you need generated file name names (and/or ancestor folders), you can use
111
+ the very same tags as those documented in previous section.
112
+
113
+ * In scaffolding mode, all files can have dynamic names.
114
+ * In snippets mode, only files with a `.jeny` ext can.
115
+
116
+ ## Atomic snippets generation
117
+
118
+ The `--git` commandline option (or equivalent configuration, see section
119
+ below) can be used to generate snippets in an atomic way.
120
+
121
+ ```sh
122
+ jeny --git -d ... s snipname
123
+ ```
124
+
125
+ Doing so will:
126
+ - stash any current change
127
+ - generate code snippets
128
+ - commit the result
129
+ - unstash stashed changes
130
+
131
+ If anything fails, all changes are reverted before unstashing.
132
+
133
+ Please also check the `--no-stash` and `--no-commit` commandline options.
134
+
135
+ **IMPORTANT** You must execute `jeny` with a git project as current folder.
136
+
137
+ **IMPORTANT** Please use this option with case, as it is still experimental.
138
+
139
+ ## Configuration and available options
140
+
141
+ Jeny behavior can be tuned in three different ways (by ascending order of
142
+ priority): environment variables, a .jeny configuration file, and command
143
+ line options.
144
+
145
+ For full details, please check the documentation of `Jeny::Configuration`.
146
+
147
+ ### Environment variables
148
+
149
+ * `JENY_EDITOR`, `GIT_EDITOR`, `EDITOR` are inspected in that order to
150
+ find which source code editor to use
151
+
152
+ * `JENY_STATE_MANAGER=none|git` is used to know which state manager to
153
+ use. See `Jeny::StateManager`
154
+
155
+ ### .jeny configuration file
156
+
157
+ At start `jeny` looks for a `.jeny` configuration file in the current
158
+ folder and all ancestors, and uses it to override default configuration
159
+ infered from environment variables.
160
+
161
+ ```
162
+ Jeny::Configuration.new do |c|
163
+ # c is a Jeny::Configuration instance
164
+ #
165
+ # many accessors are available for options
166
+ end
167
+ ```
168
+
169
+ ### Commandline options
170
+
171
+ Options passed on the commandline always override the configuration
172
+ obtained through environment variables and the .jeny file.
173
+
174
+ For a full list of options, check the help:
175
+
176
+ ```
177
+ jeny --help
178
+ ```
179
+
180
+ ## Public API
181
+
182
+ This library uses SemVer 2.0. Its public API is:
183
+
184
+ * The command line tool and its options
185
+ * The Configuration class powering the .jeny file
186
+ * All recognized environment variables and their effect on configuration
187
+ * The syntax and semantics of #jeny blocks and file header
188
+ * The syntax and semantics of the templating language
189
+
190
+ ## Contributing
191
+
192
+ Use github issues and pull requests for contributions. Please favor pull
193
+ requests if possible for small changes. Make sure to keep in touch with us
194
+ before making big changes though, as they might not be aligned with our
195
+ roadmap, or conflict with open pull requests.
5
196
 
6
197
  ## Licence
7
198
 
@@ -0,0 +1,3 @@
1
+ #/usr/bin/env ruby
2
+ require 'jeny'
3
+ Jeny::Command.call(ARGV.dup)
@@ -1,3 +1,14 @@
1
+ require 'path'
2
+ require 'wlang'
3
+ require 'ostruct'
1
4
  module Jeny
5
+ class Error < StandardError; end
2
6
  end
3
7
  require_relative 'jeny/version'
8
+ require_relative 'jeny/state_manager'
9
+ require_relative 'jeny/configuration'
10
+ require_relative 'jeny/caser'
11
+ require_relative 'jeny/dialect'
12
+ require_relative 'jeny/file'
13
+ require_relative 'jeny/code_block'
14
+ require_relative 'jeny/command'
@@ -0,0 +1,113 @@
1
+ module Jeny
2
+ module Caser
3
+
4
+ def self.for_hash(hash)
5
+ hash.each_pair.each_with_object({}) do |(k,v),memo|
6
+ case v
7
+ when String
8
+ Caser.methods(false).each do |m|
9
+ next if m == :for_hash or m == :gen_parts
10
+ memo[Caser.send(m, k)] = Caser.send(m, v)
11
+ end
12
+ when Hash
13
+ memo[k] = for_hash(v)
14
+ when Array
15
+ memo[k] = v.map{|x| for_hash(x) }
16
+ else
17
+ v
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.gen_parts(src)
23
+ src.split(/[ -_]/)
24
+ end
25
+
26
+ def flat(src)
27
+ parts = gen_parts(src) unless src.is_a?(Array)
28
+ parts.join
29
+ end
30
+ module_function :flat
31
+
32
+ def upper(src)
33
+ parts = gen_parts(src) unless src.is_a?(Array)
34
+ parts.map(&:upcase).join(" ")
35
+ end
36
+ module_function :upper
37
+
38
+ def upperflat(src)
39
+ parts = gen_parts(src) unless src.is_a?(Array)
40
+ parts.map(&:upcase).join
41
+ end
42
+ module_function :upperflat
43
+
44
+ def screaming(src)
45
+ parts = gen_parts(src) unless src.is_a?(Array)
46
+ parts.map(&:upcase).join("_")
47
+ end
48
+ module_function :screaming
49
+
50
+ def macro(src)
51
+ screaming(src)
52
+ end
53
+ module_function :macro
54
+
55
+ def constant(src)
56
+ screaming(src)
57
+ end
58
+ module_function :constant
59
+
60
+ def underscore(src)
61
+ parts = gen_parts(src) unless src.is_a?(Array)
62
+ parts.join("_")
63
+ end
64
+ module_function :underscore
65
+
66
+ def snake(src)
67
+ underscore(src)
68
+ end
69
+ module_function :snake
70
+
71
+ def camel(src)
72
+ parts = gen_parts(src) unless src.is_a?(Array)
73
+ parts.first + parts[1..-1].map(&:capitalize).join
74
+ end
75
+ module_function :camel
76
+
77
+ def pascal(src)
78
+ parts = gen_parts(src) unless src.is_a?(Array)
79
+ parts.map(&:capitalize).join
80
+ end
81
+ module_function :pascal
82
+
83
+ def kebab(src)
84
+ parts = gen_parts(src) unless src.is_a?(Array)
85
+ parts.join("-")
86
+ end
87
+ module_function :kebab
88
+
89
+ def train(src)
90
+ parts = gen_parts(src) unless src.is_a?(Array)
91
+ parts.map(&:capitalize).join("-")
92
+ end
93
+ module_function :train
94
+
95
+ def header(src)
96
+ train(src)
97
+ end
98
+ module_function :header
99
+
100
+ def cobol(src)
101
+ parts = gen_parts(src) unless src.is_a?(Array)
102
+ parts.map(&:upcase).join("-")
103
+ end
104
+ module_function :cobol
105
+
106
+ def donner(src)
107
+ parts = gen_parts(src) unless src.is_a?(Array)
108
+ parts.join("|")
109
+ end
110
+ module_function :donner
111
+
112
+ end # module Caser
113
+ end # module Jeny
@@ -0,0 +1,29 @@
1
+ module Jeny
2
+ class CodeBlock
3
+
4
+ def initialize(source, path, line, asset)
5
+ @source = source
6
+ @path = path
7
+ @line = line
8
+ @asset = asset
9
+ end
10
+ attr_reader :source, :path, :line, :asset
11
+
12
+ def line_index
13
+ line - 1
14
+ end
15
+
16
+ def instantiate(data)
17
+ case d = data[asset]
18
+ when NilClass
19
+ when Hash
20
+ Dialect.render(source, d)
21
+ when Array
22
+ d.map{|item| instantiate(asset => item) }.join("\n")
23
+ else
24
+ raise Error, "Unexpected block asset: `#{asset} = #{d}`"
25
+ end
26
+ end
27
+
28
+ end # class CodeBlock
29
+ end # module Jeny
@@ -0,0 +1,111 @@
1
+ require 'optparse'
2
+ module Jeny
3
+ class Command
4
+
5
+ def initialize
6
+ @config = nil
7
+ @jeny_data = {}
8
+ end
9
+ attr_reader :jeny_data
10
+ attr_reader :config
11
+
12
+ def self.call(argv)
13
+ new.call(argv)
14
+ rescue Error => ex
15
+ puts ex.message
16
+ exit(1)
17
+ end
18
+
19
+ def call(argv)
20
+ args = parse_argv!(argv, load_config!)
21
+ case command = args.first
22
+ when "g", "generate"
23
+ _, from, to = args
24
+ from, to = Path(from), Path(to)
25
+ raise Error, "No such template `#{from}`" unless from.directory?
26
+ to.mkdir_p
27
+ Generate.new(@config, jeny_data, from, to).call
28
+ when "s", "snippets"
29
+ _, asset, source = args
30
+ raise Error, "Asset must be specified" if asset.nil? or Path(asset).exist?
31
+ source ||= Path.pwd
32
+ Snippets.new(@config, jeny_data, asset, Path(source)).call
33
+ else
34
+ raise Error, "Unknown command `#{command}`"
35
+ end
36
+ end
37
+
38
+ def parse_argv!(argv, config = Configuration.new)
39
+ @config = config
40
+ option_parser(config).parse!(argv)
41
+ end
42
+
43
+ private
44
+
45
+ def load_config!
46
+ jeny_file = Path.pwd/".jeny"
47
+ jeny_file = Path.backfind(".jeny") unless jeny_file.file?
48
+ unless jeny_file
49
+ puts "Using default Jeny configuration"
50
+ return Configuration.new
51
+ end
52
+ unless (cf = Path(jeny_file)).file?
53
+ raise Error, "No such file `#{jeny_file}`"
54
+ end
55
+ unless (config = Kernel.eval(cf.read)).is_a?(Configuration)
56
+ raise Error, "Config file corrupted, no Configuration returned"
57
+ end
58
+ puts "Using #{jeny_file}"
59
+ config.tap{|c|
60
+ c.jeny_file = cf
61
+ }
62
+ end
63
+
64
+ def option_parser(config)
65
+ option_parser ||= OptionParser.new do |opts|
66
+ opts.banner = <<~B
67
+ Usage: jeny [options] g[enerate] SOURCE TARGET
68
+ jeny [options] s[nippets] ASSET [TARGET]
69
+ B
70
+ opts.on("-d key:value", "Add generation data") do |pair|
71
+ k, v = pair.split(':')
72
+ @jeny_data[k] = v
73
+ end
74
+ opts.on("-f datafile", "Take generation data from a file") do |file|
75
+ file = Path(file)
76
+ raise Error, "No such file: #{file}" unless file.exists?
77
+ require 'yaml' if file.ext == 'yml' || file.ext == 'yaml'
78
+ require 'json' if file.ext == 'json'
79
+ @jeny_data = file.load
80
+ end
81
+ opts.on("--git", "Use git as state manager") do
82
+ config.state_manager = :git
83
+ end
84
+ opts.on("--edit-if=MATCH", "Edit files matching a given term") do |s|
85
+ config.edit_changed_files = ->(f,c){ c =~ Regexp.new(s) }
86
+ end
87
+ opts.on("--[no-]edit", "Edit files having a TODO") do |s|
88
+ config.edit_changed_files = s ? Configuration::DEFAULT_EDIT_PROC : false
89
+ end
90
+ opts.on("--[no-]stash", "Stash before generating snippets") do |s|
91
+ config.state_manager_options[:stash] = s
92
+ end
93
+ opts.on("--[no-]commit", "Commit generated snippets") do |c|
94
+ config.state_manager_options[:commit] = c
95
+ end
96
+ opts.on("-v", "--version", "Prints version") do
97
+ puts "Jeny v#{VERSION}"
98
+ exit
99
+ end
100
+ opts.on("-h", "--help", "Prints this help") do
101
+ puts opts
102
+ exit
103
+ end
104
+ end
105
+ end
106
+
107
+ end # class Command
108
+ end # module Jeny
109
+ require_relative 'command/support'
110
+ require_relative 'command/generate'
111
+ require_relative 'command/snippets'
@@ -0,0 +1,35 @@
1
+ module Jeny
2
+ class Command
3
+ class Generate
4
+ include Support
5
+
6
+ def initialize(config, data, from, to)
7
+ @config = config
8
+ @data = Caser.for_hash(data)
9
+ @from = from
10
+ @to = to
11
+ end
12
+ attr_reader :config, :data, :from, :to
13
+
14
+ def call
15
+ puts
16
+ changed = []
17
+ from.glob("**/*") do |source|
18
+ target = target_for(source)
19
+ puts "creating #{simplify_path(target)}"
20
+ if source.directory?
21
+ target.mkdir_p
22
+ else
23
+ target.parent.mkdir_p
24
+ file = File::Full.new(source, config)
25
+ target_content = file.instantiate(data)
26
+ target.write(target_content)
27
+ changed << [target, target_content]
28
+ end
29
+ end
30
+ edit_changed_files(changed)
31
+ end
32
+
33
+ end # class Generate
34
+ end # class Command
35
+ end # module Jeny
@@ -0,0 +1,70 @@
1
+ module Jeny
2
+ class Command
3
+ class Snippets
4
+ include Support
5
+
6
+ def initialize(config, data, asset, from)
7
+ @config = config
8
+ @data = { asset => Caser.for_hash(data) }
9
+ @asset = asset
10
+ @from = from
11
+ end
12
+ attr_reader :config, :data, :asset, :from
13
+
14
+ alias :to :from
15
+
16
+ def call
17
+ puts
18
+ sm, state = config.state_manager, OpenStruct.new
19
+ sm.stash(state) if config.sm_stash?
20
+
21
+ changed = []
22
+ from.glob("**/*").each do |source|
23
+ next if source.directory?
24
+ next if config.ignore_file?(source)
25
+ pair = snippet_it(source)
26
+ changed << pair if pair
27
+ end
28
+
29
+ sm.commit(changed.map(&:first), state) if config.sm_commit?
30
+
31
+ edit_changed_files(changed)
32
+ rescue
33
+ sm.reset(changed.map(&:first), state)
34
+ raise
35
+ ensure
36
+ sm.unstash(state) if config.sm_stash?
37
+ end
38
+
39
+ def snippet_it(source)
40
+ target, target_content = nil
41
+ if source.ext =~ /\.?jeny/
42
+ file = File::Full.new(source, config)
43
+ if file.has_jeny_context?
44
+ ctx = file.instantiate_context(data)
45
+ if ctx
46
+ target_content = file.instantiate(ctx)
47
+ target = target_for(source, ctx)
48
+ target.parent.mkdir_p
49
+ target.write(target_content)
50
+ puts "snippets #{simplify_path(target)}"
51
+ end
52
+ end
53
+ else
54
+ file = File::WithBlocks.new(source, config)
55
+ if file.has_jeny_blocks?
56
+ target_content = file.instantiate(data)
57
+ target = target_for(source)
58
+ target.write(target_content)
59
+ puts "snippets #{simplify_path(target)}"
60
+ end
61
+ end
62
+ target ? [target, target_content] : nil
63
+ rescue => ex
64
+ msg = "Error in `#{simplify_path(source)}`: #{ex.message}"
65
+ raise Error, msg
66
+ end
67
+
68
+ end # class Snippets
69
+ end # class Command
70
+ end # module Jeny
@@ -0,0 +1,30 @@
1
+ module Jeny
2
+ class Command
3
+ module Support
4
+
5
+ def target_for(source, data = self.data)
6
+ relative = source.relative_to(from)
7
+ relative.each_filename.map{|f|
8
+ Dialect.render(f.gsub(/.jeny$/, ""), data)
9
+ }.inject(to){|t,part| t/part }
10
+ end
11
+
12
+ def simplify_path(path)
13
+ if path.to_s.start_with?(Path.pwd.to_s)
14
+ path.relative_to(Path.pwd)
15
+ else
16
+ path
17
+ end
18
+ end
19
+ module_function :simplify_path
20
+
21
+ def edit_changed_files(changed)
22
+ to_open = changed
23
+ .select{|pair| config.should_be_edited?(*pair) }
24
+ .map{|pair| simplify_path(pair.first) }
25
+ config.open_editor(to_open) unless to_open.empty?
26
+ end
27
+
28
+ end # module Support
29
+ end # class Command
30
+ end # module Jeny
@@ -0,0 +1,132 @@
1
+ module Jeny
2
+ class Configuration
3
+
4
+ DEFAULT_EDIT_PROC = ->(_,content) {
5
+ content =~ /TODO/
6
+ }
7
+
8
+ def initialize
9
+ @jeny_block_delimiter = "#jeny"
10
+ @ignore_pattern = /^(vendor|\.bundle)/
11
+ @editor_command = default_editor_command
12
+ @edit_changed_files = DEFAULT_EDIT_PROC
13
+ @state_manager = default_state_manager
14
+ @state_manager_options = {
15
+ stash: true,
16
+ commit: true
17
+ }
18
+ yield(self) if block_given?
19
+ end
20
+ attr_accessor :jeny_file
21
+
22
+ # The delimiter used for jeny block in source code files.
23
+ #
24
+ # Defaults to `#jeny`
25
+ attr_accessor :jeny_block_delimiter
26
+
27
+ # Regular expression matching files that can always be ignored by Snippets.
28
+ #
29
+ # Defaults to /^(vendor|\.bundle)/
30
+ attr_accessor :ignore_pattern
31
+
32
+ # Shell command to open the source code editor.
33
+ #
34
+ # Default value checks the JENY_EDITOR, GIT_EDITOR, EDITOR
35
+ # environment variables, and fallbacks to "code".
36
+ attr_accessor :editor_command
37
+
38
+ def default_editor_command
39
+ ENV['JENY_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
40
+ end
41
+
42
+ # State manager to use.
43
+ #
44
+ # Default value check the JENY_STATE_MANAGER environment variable:
45
+ # - `none`, no state management is done
46
+ # - `git`, git is used to stash/unstash/commit/reset
47
+ #
48
+ # Defaults to `none`, that is, to an empty state manager.
49
+ attr_reader :state_manager
50
+
51
+ # Options for the state manager.
52
+ #
53
+ # This is a Hash, with `:stash` and `:commit` keys mapping to
54
+ # either true of false.
55
+ #
56
+ # Both are true by default.
57
+ attr_reader :state_manager_options
58
+
59
+ # :nodoc:
60
+ def sm_stash?
61
+ state_manager_options[:stash]
62
+ end
63
+
64
+ # :nodoc:
65
+ def sm_commit?
66
+ state_manager_options[:commit]
67
+ end
68
+
69
+ # Sets the state manager to use. `sm` can be a state manager instance,
70
+ # of a string of symbol with same value as JENY_STATE_MANAGER env
71
+ # variable.
72
+ def state_manager=(sm)
73
+ @state_manager = case sm
74
+ when StateManager then sm
75
+ when :git, "git" then StateManager::Git.new(self)
76
+ else StateManager.new(self)
77
+ end
78
+ end
79
+
80
+ def default_state_manager
81
+ case ENV['JENY_STATE_MANAGER']
82
+ when "git"
83
+ StateManager::Git.new(self)
84
+ else
85
+ StateManager.new(self)
86
+ end
87
+ end
88
+
89
+ # Whether files generated/modified must be edited right after.
90
+ #
91
+ # Accepted values are:
92
+ # - `false`, then source code edition is disabled
93
+ # - `true`, then all files are open after being snipetted
94
+ # - `/.../`, then only files whose name match the regexp are open
95
+ # - `->(f,c){}`, the file `f` whose generated content is `c` is open
96
+ # is the proc returns a truthy value.
97
+ #
98
+ # Defaults to a Proc that opens files having at least one TODO.
99
+ attr_accessor :edit_changed_files
100
+
101
+ alias :edit_changed_files? :edit_changed_files
102
+
103
+ # Should `file` be ignored?
104
+ def ignore_file?(file)
105
+ file = Command::Support.simplify_path(file)
106
+ file.to_s =~ ignore_pattern
107
+ end
108
+
109
+ # Whether file should be edited after being snippetted
110
+ def should_be_edited?(file, content)
111
+ return false if editor_command.nil?
112
+ case edit_changed_files
113
+ when false, true, nil
114
+ !!edit_changed_files
115
+ when Regexp
116
+ !!(file.to_s =~ edit_changed_files)
117
+ when Proc
118
+ !!edit_changed_files.call(file, content)
119
+ else
120
+ raise Error, "Wrong edit_changed_files `#{edit_changed_files}`"
121
+ end
122
+ end
123
+
124
+ def open_editor(files)
125
+ unless editor_command
126
+ raise Error, "source code editor is not enabled"
127
+ end
128
+ fork{ exec(editor_command + " " + files.join(" ")) }
129
+ end
130
+
131
+ end # class Configuration
132
+ end # module Jeny
@@ -0,0 +1,12 @@
1
+ module Jeny
2
+ class Dialect < WLang::Dialect
3
+
4
+ def varvalue(buf, fn)
5
+ var_name = render(fn)
6
+ var_value = evaluate(var_name)
7
+ buf << var_value.to_s
8
+ end
9
+ tag '$', :varvalue
10
+
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ module Jeny
2
+ class File
3
+
4
+ def initialize(path, config)
5
+ @path = path
6
+ @config = config
7
+ end
8
+ attr_reader :path, :config
9
+
10
+ def rewrite(data, to)
11
+ to.write(instantiate(data))
12
+ end
13
+
14
+ end # class File
15
+ end # module Jeny
16
+ require_relative 'file/with_blocks'
17
+ require_relative 'file/full'
@@ -0,0 +1,30 @@
1
+ module Jeny
2
+ class File
3
+ class Full < File
4
+
5
+ def context_rgx
6
+ /^#{config.jeny_block_delimiter}\(([a-z]+)\)\s*$/
7
+ end
8
+
9
+ def has_jeny_context?
10
+ path.readlines.first =~ context_rgx
11
+ end
12
+
13
+ def instantiate_context(data)
14
+ if path.readlines.first =~ context_rgx
15
+ data[$1]
16
+ else
17
+ data
18
+ end
19
+ end
20
+
21
+ def instantiate(data)
22
+ path.readlines.map{|l|
23
+ next if l =~ context_rgx
24
+ Dialect.render(l, data)
25
+ }.compact.join("")
26
+ end
27
+
28
+ end # class Full
29
+ end # class File
30
+ end # module Jeny
@@ -0,0 +1,71 @@
1
+ module Jeny
2
+ class File
3
+ class WithBlocks < File
4
+
5
+ def has_jeny_blocks?
6
+ !jeny_blocks.empty?
7
+ end
8
+
9
+ def jeny_blocks
10
+ @jeny_blocks ||= _parse_jeny_blocks(path.readlines, 0, [])
11
+ end
12
+
13
+ def instantiate(data)
14
+ return path.read unless has_jeny_blocks?
15
+ _instantiate(data, path.readlines, 0, jeny_blocks, []).join("")
16
+ end
17
+
18
+ private
19
+
20
+ def _parse_jeny_blocks(lines, index, blocks)
21
+ if index >= lines.size
22
+ blocks
23
+ elsif lines[index] =~ jeny_block_regex
24
+ source, new_index = _parse_jeny_block(lines, index)
25
+ blocks << CodeBlock.new(source, path, index+1, $1)
26
+ _parse_jeny_blocks(lines, new_index+1, blocks)
27
+ else
28
+ _parse_jeny_blocks(lines, index+1, blocks)
29
+ end
30
+ end
31
+
32
+ def _parse_jeny_block(lines, index, str = "")
33
+ if lines[index] =~ jeny_block_regex
34
+ _parse_jeny_block(lines, index+1, str + lines[index])
35
+ else
36
+ source = str.split("\n").map{|s|
37
+ s.gsub(jeny_block_gsub, "")
38
+ }
39
+ source = source.size == 1 ? source.first : source.join("\n")+"\n"
40
+ [source, index]
41
+ end
42
+ end
43
+
44
+ def _instantiate(data, lines, index, blocks, acc)
45
+ block = blocks.first
46
+ if block.nil?
47
+ lines[index..-1].each{|l| acc << l }
48
+ acc
49
+ elsif block.line_index == index
50
+ if i = block.instantiate(data)
51
+ acc << i << "\n"
52
+ end
53
+ acc << lines[index]
54
+ _instantiate(data, lines, index+1, blocks[1..-1], acc)
55
+ else
56
+ lines[index...block.line_index].each{|l| acc << l }
57
+ _instantiate(data, lines, block.line_index, blocks, acc)
58
+ end
59
+ end
60
+
61
+ def jeny_block_regex
62
+ @jeny_block_regex ||= %r{^\s*#{config.jeny_block_delimiter}\(([a-z]+)\)}m
63
+ end
64
+
65
+ def jeny_block_gsub
66
+ @jeny_block_gsub ||= %r{#{config.jeny_block_delimiter}(\([a-z]+\))[ ]?}
67
+ end
68
+
69
+ end # class WithBlocks
70
+ end # class File
71
+ end # module Jeny
@@ -0,0 +1,51 @@
1
+ module Jeny
2
+ # State Manager abstraction used by Snippets to work atomically.
3
+ #
4
+ # An implementation is supposed to be stateless (no instance
5
+ # variables). The methods receive a `state` argument, which is
6
+ # an OpenStruct that can be used to track state accros calls.
7
+ #
8
+ # See StateManager::Git for a typical implementation using git.
9
+ class StateManager
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+ attr_reader :config
15
+
16
+ # Make sure the working directory is clean, for instance by stashing
17
+ # any pending change.
18
+ #
19
+ # The method MAY raise an Error if the current state is not clean and
20
+ # nothing can be done about it.
21
+ #
22
+ # The method SHOULD NOT raise an Error is nothing needs to be done.
23
+ def stash(state)
24
+ end
25
+
26
+ # Unstash changes stashed the last time `stash` has been called.
27
+ #
28
+ # This method MAY NOT raise errors since a previous `stash`
29
+ # has been successfuly done earlier.
30
+ #
31
+ # The method SHOULD NOT raise an Error is nothing needs to be done.
32
+ def unstash(state)
33
+ end
34
+
35
+ # Reset all changes to files in `changed`, typically because an error
36
+ # occured.
37
+ #
38
+ # The method SHOULD NOT raise an Error in any case.
39
+ def reset(changed, state)
40
+ end
41
+
42
+ # Commit all changes to the files in `changed`.
43
+ #
44
+ # The method MAY raise an Error, but it will force jeny to reset
45
+ # everything.
46
+ def commit(changed, state)
47
+ end
48
+
49
+ end # class StateManage
50
+ end # module Jeny
51
+ require_relative 'state_manager/git'
@@ -0,0 +1,39 @@
1
+ module Jeny
2
+ class StateManager
3
+ # StateManager implementation that uses git to manage state management.
4
+ #
5
+ # This state management requires executing jeny in the git root folder.
6
+ class Git < StateManager
7
+
8
+ # Executes a `git stash`
9
+ def stash(state)
10
+ system("git diff --exit-code")
11
+ state.stashed = ($?.exitstatus != 0)
12
+ system("git stash") if state.stashed
13
+ end
14
+
15
+ # Executes a `git stash pop`
16
+ def unstash(state)
17
+ system("git stash pop") if state.stashed
18
+ end
19
+
20
+ # Reset all changes through a `git reset --hard`.
21
+ #
22
+ # WARN: changes not related to `changed` are reverted too,
23
+ # which should be nothing since a stash has been done before.
24
+ def reset(changed, state)
25
+ changed.each{|f| f.rm_rf if f.exists? }
26
+ system("git reset --hard")
27
+ end
28
+
29
+ # Commits all changes, using `git add` the `git commit`
30
+ def commit(changed, state)
31
+ return if changed.empty?
32
+ system("git add #{changed.join(" ")}")
33
+ msg = "jeny #{$*.join(' ')}"
34
+ system("git commit -m '#{msg}'")
35
+ end
36
+
37
+ end # class Git
38
+ end # class StateManager
39
+ end # module Jeny
@@ -1,7 +1,7 @@
1
1
  module Jeny
2
2
  module Version
3
- MAJOR = 0
4
- MINOR = 1
3
+ MAJOR = 1
4
+ MINOR = 0
5
5
  TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
@@ -6,10 +6,17 @@ namespace :test do
6
6
  desc "Runs unit tests"
7
7
  RSpec::Core::RakeTask.new(:unit) do |t|
8
8
  t.pattern = "spec/unit/**/test_*.rb"
9
- t.rspec_opts = ["-Ilib", "-Ispec/unit", "--color", "--backtrace", "--format=progress"]
9
+ t.rspec_opts = ["-Ilib", "-Ispec", "--color", "--backtrace", "--format=progress"]
10
10
  end
11
11
  tests << :unit
12
12
 
13
+ desc "Runs command tests"
14
+ RSpec::Core::RakeTask.new(:command) do |t|
15
+ t.pattern = "spec/command/**/test_*.rb"
16
+ t.rspec_opts = ["-Ilib", "-Ispec", "--color", "--backtrace", "--format=progress"]
17
+ end
18
+ tests << :command
19
+
13
20
  task :all => tests
14
21
  end
15
22
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jeny
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bernard Lambeau
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-29 00:00:00.000000000 Z
11
+ date: 2020-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: path
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: wlang
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -54,7 +68,8 @@ dependencies:
54
68
  version: '3.7'
55
69
  description: Simple scaffolding and code generation
56
70
  email: blambeau@gmail.com
57
- executables: []
71
+ executables:
72
+ - jeny
58
73
  extensions: []
59
74
  extra_rdoc_files: []
60
75
  files:
@@ -62,7 +77,21 @@ files:
62
77
  - LICENSE.md
63
78
  - README.md
64
79
  - Rakefile
80
+ - bin/jeny
65
81
  - lib/jeny.rb
82
+ - lib/jeny/caser.rb
83
+ - lib/jeny/code_block.rb
84
+ - lib/jeny/command.rb
85
+ - lib/jeny/command/generate.rb
86
+ - lib/jeny/command/snippets.rb
87
+ - lib/jeny/command/support.rb
88
+ - lib/jeny/configuration.rb
89
+ - lib/jeny/dialect.rb
90
+ - lib/jeny/file.rb
91
+ - lib/jeny/file/full.rb
92
+ - lib/jeny/file/with_blocks.rb
93
+ - lib/jeny/state_manager.rb
94
+ - lib/jeny/state_manager/git.rb
66
95
  - lib/jeny/version.rb
67
96
  - tasks/gem.rake
68
97
  - tasks/test.rake
@@ -70,7 +99,7 @@ homepage: http://github.com/enspirit/jeny
70
99
  licenses:
71
100
  - MIT
72
101
  metadata: {}
73
- post_install_message:
102
+ post_install_message:
74
103
  rdoc_options: []
75
104
  require_paths:
76
105
  - lib
@@ -85,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
114
  - !ruby/object:Gem::Version
86
115
  version: '0'
87
116
  requirements: []
88
- rubygems_version: 3.2.1
89
- signing_key:
117
+ rubygems_version: 3.1.4
118
+ signing_key:
90
119
  specification_version: 4
91
120
  summary: Simple scaffolding and code generation
92
121
  test_files: []