Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for package-provided project templates #908

Merged
merged 44 commits into from Dec 21, 2016

Conversation

kevinushey
Copy link
Contributor

@kevinushey kevinushey commented Nov 21, 2016

NOTE: PR not quite ready for merge, but most components ready for review.


This PR implements support for package-provided project templates. This PR basically provides an interface for RStudio to discover and call various 'skeleton' functions that packages can provide. A brief look:

screen shot 2016-11-21 at 2 49 03 pm

Packages can define a project template by creating one or more skeleton descriptions in the rstudio/templates folder. The project template descriptions should be DCF files with the form e.g.

Binding: Rcpp.package.skeleton
Title: R Package using Rcpp
Subtitle: Create a new R Package using Rcpp
Caption: Create R package using Rcpp

Parameter: module
Widget: CheckBox

That is, each DCF file should contain an initial DCF record containing information about the skeleton function, and then 0 or more descriptions of widgets that can be used to power the widget.

TODO

  • Ensure that the index is updated when the library is changed (as we do with addins)
  • Ensure that errors are handled / reported well. (we might want to consider some kind of diagnostics output when editing / working with a dcf file in the rstudio/templates folder)
  • Consider generating Subtitle, Caption based on Title field.
  • Allow for package skeletons provided in ~/.R/rstudio/templates directory?
  • Add support for icons.
  • Ensure that the project template registry is received on browser refresh / reload.
  • Add support for default values / selections for select inputs?
    ...

@@ -138,6 +138,19 @@ std::pair<std::vector<std::string>, InputIterator> parseCsvLine(
std::vector<std::string>(), begin);
}

template <typename InputIterator>
InputIterator parseCsvLine(InputIterator begin,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this overload primarily because the main version that returns a std::pair<> can often be cumbersome to work with.

LOG_ERROR(error);

core::json::fillVectorString(
object["fields"].get_array(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC this will throw if the object does not contain fields, and also if fields is not an array, so you might consider a two-stage access here for safety (get the JSON array above then coerce to a string vector here)

"label", &ptwd.label);

if (error)
LOG_ERROR(error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be better here to return the error (and take a pointer to a ptwd as a parameter) so that the caller knows not to process the template if it's malformed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea -- I'll make that change.

else if (key == "Icon")
{
// read icon file from disk
core::FilePath iconPath = resourcePath.parent().complete(value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth defending against pathologically large files here by skipping icons larger than some reasonable size (we don't want to accidentally try to stream megabytes of base64 data to the client).

if (error)
LOG_ERROR(error);

core::json::Array widgetsJson = object["widgets"].get_array();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above w/ fields (this can throw exceptions)

private:
void onIndexingStarted()
{
pRegistry_ = boost::make_shared<ProjectTemplateRegistry>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this shared pointer manages the lifetime of the globalProjectTemplateRegistry. If that's the case then does resetting it cause the pointer to release the registry, so that there is briefly no registry until indexing completes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite -- the indexer actually contains its own registry (and maintains a shared pointer to that); after indexing has completed the global registry is updated based on that reference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the object ever actually get copied, though? If not the global registry is a reference to an object on the heap that's owned only by the shared pointer. Let's discuss realtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion -- indeed you're right! The global registry needs to be represented as a shared pointer; in addition I'm missing a noncopyable inheritance on the registry class. Thanks for pointing this out!

return Success();

std::string reason =
"invalid project template description: missing or empty fields "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also log the location of the file in which these fields were expected? You can just add this as a property on the error; otherwise I think the message in the logs will be hard to associate with a particular package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

pDescription->subtitle = "Create a new " + title;

if (pDescription->subtitle.empty())
pDescription->caption = "Create " + title;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: dead code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops! This should be checking the caption field; thanks for catching this.

@jmcphers
Copy link
Member

jmcphers commented Nov 22, 2016

I think many project skeletons are going to wind up including sample files with boilerplate code. For some projects it will be necessary to run R code to generate these, but I think most can just include them declaratively. We might want to add facilities for:

  • Indicating a path to a directory tree of files which will be copied into the new project by convention (bonus points if we allow some simple templating or string substitution in file names and/or contents)

  • Indicating a set of files that should be open when the new project first starts up; this is a common user request and we already have machinery for it (it's done for e.g. new Shiny projects)

Also worth considering:

  • What mechanism would you recommend to an RStudio Server admin who wants to supply their users with templates? (Of course they could make an R package and install it somewhere in the global library, just wondering if there's something else we could do with less overhead)

  • Could we implement some of our existing new project templates using this system? I think that at a minimum it would be pretty straightforward to do this with Shiny, especially if we have some package-less way of discovering templates (would also help w/ above)

@kevinushey
Copy link
Contributor Author

kevinushey commented Nov 22, 2016

Indicating a path to a directory tree of files which will be copied into the new project by convention (bonus points if we allow some simple templating or string substitution in file names and/or contents)

I'm slightly more inclined to delegate this responsibility to the skeleton function; any reason in particular why we should take control here?

Indicating a set of files that should be open when the new project first starts up; this is a common user request and we already have machinery for it (it's done for e.g. new Shiny projects)

Definitely a good idea!

What mechanism would you recommend to an RStudio Server admin who wants to supply their users with templates? (Of course they could make an R package and install it somewhere in the global library, just wondering if there's something else we could do with less overhead)

I was also thinking of parsing project templates in a preset folder, e.g. ~/.R/rstudio/templates, but I'm open to suggestions here as well. We might also allow this path to be configured in other ways -- a UI preference, an environment variable, but I'm not yet sure what would be best.

Could we implement some of our existing new project templates using this system? I think that at a minimum it would be pretty straightforward to do this with Shiny, especially if we have some package-less way of discovering templates (would also help w/ above)

Yes, I think the Shiny package skeleton we have could be replaced with a pure project template version. However, note that currently all of these skeleton functions create a brand new project; the 'New Shiny Application' dialog merely creates some new files (ie it's less heavyweight).

@jjallaire
Copy link
Member

Some top-level comments prior to code review:

  1. I agree that the skeleton function should be the one that does the file copying, substitution, etc. This enables these functions to work both within and outside of the IDE. This however does make it slightly harder for less sophisticated developers (e.g. people creating project templates inside a company) to implement a package template. Maybe we can just provide some example code as part of the documentation to ease this along.

  2. Major +1 to specifying files to open! Note that in some cases the names of files will be dynamic based on variables (including the name of the project, I'm thinking here of what happens in htmlwidgets::createWidget) so we may need some simple variable substitution within the DCF file.

  3. In terms of centralized deployment, I think that we should continue to rely on packages for this rather than defining a preset folder. This is because these templates are intended to execute code and to me code should be deployed in a package. Note that for addins and R Markdown templates (not even to speak of just common/shared libraries!) you already need to centrally deploy packages so I think this is something that every moderately sophisticated environment will have a handle on (and we can certainly help this along with documentation and product features).

  4. Agree w/ Kevin that the "New Shiny App" doesn't quite fit b/c it doesn't create a package. Note that Kevin and I have discussed extending this framework to "New File" however that's on the bubble for this release (have other things that might be more pressing). I do think that we should implement some templates "internally" (e.g. Rcpp, devtools, htmlwidgets, etc.) and then have a mechanism where we can start delegating to the package once it provides a DCF file.

  5. I think the directory should be rstudio/templates/project to accommodate the possibility of non-project (e.g. file) templates.

@kevinushey
Copy link
Contributor Author

Whoops, yes -- sorry, I flipped the order!

@kevinushey
Copy link
Contributor Author

Some thoughts on how the user might specify what files should be opened when the new project is opened:

OpenFiles: R/server.R, R/ui.R, README-%s

Ie, the OpenFiles field can be a CSV line; the paths will be resolved relative to the new project root. The simplest form of templating we could do is just substitute e.g. %s with the project name (name of created folder); any thoughts on whether we want to be more opinionated / provide something a bit different? Some other candidates:

1) OpenFiles: README-${PACKAGE}     # shell-style
2) OpenFiles: README-{{ package }}  # whisker-style
3) OpenFiles: README-%s             # sprintf-style

One thing that would be nice about an environment-variable based substitution -- the skeleton function could just set some environment variables that would could then be read by RStudio when forming the template file paths.

We could also consider supporting globs, e.g.

OpenFiles: R/*.R

@jjallaire
Copy link
Member

I'd say support globs and support shell-style variables. Variables should
include the package name and any of the other input variables specified.

On Tue, Nov 22, 2016 at 2:09 PM, Kevin Ushey notifications@github.com
wrote:

Some thoughts on how the user might specify what files should be opened
when the new project is opened:

OpenFiles: R/server.R, R/ui.R, README-%s

Ie, the OpenFiles field can be a CSV line; the paths will be resolved
relative to the new project root. The simplest form of templating we could
do is just substitute e.g. %s with the project name (name of created
folder); any thoughts on whether we want to be more opinionated / provide
something a bit different? Some other candidates:

  1. OpenFiles: README-${PACKAGE} # shell-style
  2. OpenFiles: README-{{ package }} # whisker-style
  3. OpenFiles: README-%s # sprintf-style

One thing that would be nice about an environment-variable based
substitution -- the skeleton function could just set some environment
variables that would could then be read by RStudio when forming the
template file paths.

We could also consider supporting globs, e.g.

OpenFiles: R/*.R


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
#908 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAGXx9rLLMGLFiKInzrYYJZ6dnYzSTWKks5rAz3UgaJpZM4K4ydg
.

}

template <typename T>
core::Error readObject(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method (+ the one above) allow us to more easily read vectors of things, e.g. vectors of strings.

@kevinushey
Copy link
Contributor Author

I think this PR is ready for a more formal code review now. @jmcphers, if you have time would you be willing to take a look?

else
// if we have a custom project template, call that first
if (!projectTemplateOptions.is_null())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sanity, consider making sure its type is json::Object here too (otherwise it looks like initializeProjectFromTemplate can throw)


// execute pending callbacks
for (Callback callback : pendingCallbacks_)
callback.execute(registry_);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could double evaluate if a callback throws while executing -- maybe wrap in a try catch and/or remove the callback before executing?

@kevinushey
Copy link
Contributor Author

@jjallaire, do you want to review / sanity check before merge? (We can also tweak + update things on master as needed)

@jjallaire
Copy link
Member

jjallaire commented Dec 21, 2016 via email

@jjallaire jjallaire merged commit 2ca163a into master Dec 21, 2016
@valerie-rstudio valerie-rstudio deleted the feature/project-template-indexer branch January 21, 2022 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants