jeny 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: []